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


191

ฉันกำลังวัดข้อความโดยใช้Paint.getTextBounds()เนื่องจากฉันสนใจที่จะให้ทั้งความสูงและความกว้างของข้อความแสดงผล อย่างไรก็ตามข้อความจริงที่แสดงอยู่เสมอ bit กว้างกว่า.width()ของข้อมูลที่เต็มไปด้วยRectgetTextBounds()

ฉันประหลาดใจเมื่อฉันทดสอบ.measureText()และพบว่ามันส่งกลับค่า (สูงกว่า) ที่แตกต่างกัน ฉันลองดูและพบว่าถูกต้อง

เหตุใดพวกเขาจึงรายงานความกว้างที่แตกต่างกัน ฉันจะรับความสูงและความกว้างได้อย่างไร ฉันหมายความว่าฉันสามารถใช้.measureText()แต่แล้วฉันจะไม่ทราบว่าผมควรจะไว้วางใจกลับโดย.height()getTextBounds()

ตามที่ร้องขอนี่คือรหัสขั้นต่ำในการทำให้เกิดปัญหาอีกครั้ง:

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

ผลลัพธ์แสดงให้เห็นว่าความแตกต่างไม่เพียง แต่จะได้รับมากกว่า 1 (และไม่มีข้อผิดพลาดในการปัดเศษในนาทีสุดท้าย) แต่ดูเหมือนว่าจะเพิ่มขึ้นตามขนาด (ฉันกำลังจะสรุปมากขึ้น แต่อาจขึ้นอยู่กับแบบอักษรทั้งหมด):

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

คุณสามารถเห็น [ทางของฉัน] [1] ที่สามารถรับตำแหน่งและขอบเขตที่ถูกต้องได้ [1]: stackoverflow.com/a/19979937/1621354
Alex Chi

คำอธิบายที่เกี่ยวข้องเกี่ยวกับวิธีการวัดข้อความสำหรับผู้เข้าชมคนอื่น ๆ สำหรับคำถามนี้
Suragch

คำตอบ:


370

คุณสามารถทำสิ่งที่ฉันทำเพื่อตรวจสอบปัญหาดังกล่าว:

ศึกษาซอร์สโค้ด Android, แหล่ง Paint.java ดูทั้งเมธอด measureText และ getTextBounds คุณจะได้เรียนรู้ว่าการวัดวัดเรียก native_measureText และ getTextBounds เรียก nativeGetStringBounds ซึ่งเป็นวิธีการดั้งเดิมที่ใช้ใน C ++

ดังนั้นคุณควรศึกษา Paint.cpp ต่อไปซึ่งใช้ทั้งสองอย่าง

native_measureText -> SkPaintGlue :: measureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

ตอนนี้การศึกษาของคุณตรวจสอบว่าวิธีการเหล่านี้แตกต่างกันอย่างไร หลังจากตรวจสอบพารามิเตอร์แล้วทั้งสองฟังก์ชั่นการโทร SkPaint :: measureText ใน Skia Lib (ส่วนหนึ่งของ Android) แต่พวกเขาทั้งคู่เรียกฟอร์มที่มีการโอเวอร์โหลดที่แตกต่างกัน

ขุดลงไปใน Skia ต่อไปฉันเห็นว่าการโทรทั้งสองส่งผลให้มีการคำนวณแบบเดียวกันในฟังก์ชั่นเดียวกัน แต่จะให้ผลลัพธ์ที่ต่างกันเท่านั้น

ในการตอบคำถามของคุณ: การ โทรของคุณทั้งสองทำการคำนวณแบบเดียวกัน ความแตกต่างของผลลัพธ์ที่เป็นไปได้คือข้อเท็จจริงที่ว่าgetTextBoundsคืนค่าขอบเขตเป็นจำนวนเต็มในขณะที่measureTextส่งคืนค่าทศนิยม

ดังนั้นสิ่งที่คุณได้รับคือการปัดเศษข้อผิดพลาดในระหว่างการแปลง float เป็น int และสิ่งนี้เกิดขึ้นใน Paint.cpp ใน SkPaintGlue :: doTextBounds เพื่อเรียกใช้ฟังก์ชัน SkRect :: roundOut

ความแตกต่างระหว่างความกว้างที่คำนวณได้ของการโทรสองสายนั้นอาจเป็นได้สูงสุด 1

แก้ไข 4 ต.ค. 2554

สิ่งที่อาจจะดีกว่าการสร้างภาพ ฉันใช้ความพยายามในการสำรวจของตัวเองและเพื่อความโปรดปราน :)

ป้อนคำอธิบายรูปภาพที่นี่

นี่คือขนาดตัวอักษร 60 ในสีแดงเป็นขอบเขตสี่เหลี่ยมผืนผ้าในสีม่วงเป็นผลมาจากการวัดข้อความ

จะเห็นว่าขอบเขตด้านซ้ายเริ่มบางพิกเซลจากด้านซ้ายและค่าของ measureText จะเพิ่มขึ้นตามค่านี้ทั้งซ้ายและขวา นี่คือสิ่งที่เรียกว่าค่า AdvanceX ของ Glyph (ฉันได้ค้นพบสิ่งนี้ในแหล่ง Skia ใน SkPaint.cpp)

ดังนั้นผลลัพธ์ของการทดสอบก็คือ MeasureText จะเพิ่มมูลค่าล่วงหน้าให้กับข้อความทั้งสองด้านขณะที่ getTextBounds จะคำนวณขอบเขตที่น้อยที่สุดที่ข้อความจะได้รับ

หวังว่าผลลัพธ์นี้มีประโยชน์สำหรับคุณ

รหัสการทดสอบ:

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

3
ขออภัยที่ใช้เวลานานในการแสดงความคิดเห็นฉันมีสัปดาห์ที่วุ่นวาย ขอบคุณที่สละเวลาขุดลงในรหัส C ++! น่าเสียดายที่ความแตกต่างมากกว่า 1 และจะเกิดขึ้นแม้ว่าจะใช้ค่าที่ปัดเศษ - เช่นการตั้งค่าขนาดข้อความเป็น 29.0 ผลลัพธ์ใน measureText () = 263 และ getTextBounds (). width = 260
slezica

โปรดโพสต์โค้ดขั้นต่ำด้วยการตั้งค่าและการวัดสีที่สามารถทำให้เกิดปัญหาได้อีกครั้ง
ตัวชี้ Null

Updated ขออภัยในความล่าช้าอีกครั้ง
slezica

ฉันรวยที่นี่ การวิจัยที่ยอดเยี่ยม! สมควรได้รับและได้รับรางวัลอย่างเต็มที่ ขอบคุณสำหรับเวลาและความพยายามของคุณ!
slezica

16
@ หนูฉันเพิ่งพบคำถามนี้อีกครั้ง (ฉันเป็น OP) ฉันลืมไปแล้วว่าคำตอบของคุณช่างเหลือเชื่อเหลือเกิน ขอบคุณจากอนาคต! สุดยอด 100rp ฉันได้ลงทุนไปแล้ว!
slezica

21

ประสบการณ์ของฉันกับสิ่งนี้คือการที่getTextBoundsจะคืนค่า rect bounding minimized absolute ที่ห่อหุ้มข้อความไม่จำเป็นต้องเป็นความกว้างที่วัดได้เมื่อใช้การเรนเดอร์ ฉันอยากจะบอกว่าmeasureTextสมมติหนึ่งบรรทัด

เพื่อให้ได้ผลการวัดที่แม่นยำคุณควรใช้การStaticLayoutแสดงข้อความและดึงการวัดออกมา

ตัวอย่างเช่น:

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();

ไม่ทราบเกี่ยวกับ StaticLayout! getWidth () ไม่ทำงานมันแค่คืนสิ่งที่ฉันส่งผ่านไปใน costructor (ซึ่งก็คือwidthไม่ใช่maxWidth) แต่ getLineWith () จะ ฉันยังพบวิธีการคงที่ getDesiredWidth () แม้ว่าจะไม่มีความสูงเทียบเท่า โปรดแก้ไขคำตอบ! นอกจากนี้ฉันอยากจะรู้ว่าทำไมวิธีการที่แตกต่างกันมากมายให้ผลลัพธ์ที่แตกต่างกัน
slezica

ฉันไม่ดีเกี่ยวกับความกว้าง หากคุณต้องการความกว้างของข้อความที่จะแสดงในบรรทัดเดียวmeasureTextควรทำงานได้ดี มิฉะนั้นถ้าคุณรู้ว่ากลุ่มคนที่มีความกว้าง (เช่นในonMeasureหรือonLayoutคุณสามารถใช้วิธีแก้ปัญหาที่ฉันโพสต์ไว้ด้านบนคุณพยายามปรับขนาดข้อความอัตโนมัติหรือไม่นอกจากนี้เหตุผลขอบเขตข้อความก็เล็กกว่าเล็กน้อยเสมอ เพียงส่วนที่ถูก จำกัด ขอบเขตที่เล็กที่สุดที่จำเป็นสำหรับการวาดภาพ
Chase

ฉันพยายามปรับขนาด TextView โดยอัตโนมัติ ฉันต้องการความสูงเช่นกันนั่นเป็นจุดเริ่มต้นของปัญหาของฉัน! measureWidth () ทำงานได้ดีเป็นอย่างอื่น หลังจากนั้นฉันอยากรู้ว่าทำไมวิธีการที่แตกต่างกันให้คุณค่าที่แตกต่าง
slezica

1
สิ่งหนึ่งที่ต้องทราบก็คือ textview ที่ล้อมรอบและเป็น multiline จะวัดที่ match_parent สำหรับความกว้างและไม่ใช่แค่ความกว้างที่ต้องการซึ่งทำให้ความกว้างด้านบนเป็นพารามิเตอร์ที่ถูกต้องกับ StaticLayout หากคุณต้องการเครื่องมือปรับขนาดข้อความลองดูโพสต์ของฉันที่stackoverflow.com/questions/5033012/…
Chase

ฉันต้องมีแอนิเมชั่นการขยายข้อความและสิ่งนี้ช่วยให้มัด! ขอบคุณ!
กล่อง

18

คำตอบของหนูดีมาก ... และนี่คือคำอธิบายของปัญหาจริง:

คำตอบง่ายๆสั้น ๆ ว่าPaint.getTextBounds(String text, int start, int end, Rect bounds)ผลตอบแทนที่ไม่ได้เริ่มต้นที่Rect (0,0)นั่นคือเพื่อให้ได้ความกว้างจริงของข้อความที่จะถูกตั้งค่าโดยการโทรCanvas.drawText(String text, float x, float y, Paint paint)ด้วยวัตถุ Paint เดียวกันจาก getTextBounds () คุณควรเพิ่มตำแหน่งด้านซ้ายของ Rect อะไรแบบนั้น:

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

สังเกตสิ่งนี้bounds.left- นี่คือกุญแจของปัญหา

Canvas.drawText()ด้วยวิธีนี้คุณจะได้รับความกว้างเดียวกันของข้อความที่คุณจะได้รับใช้

และฟังก์ชั่นเดียวกันควรจะได้รับheightข้อความ:

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: ฉันไม่ได้ทดสอบรหัสที่แน่นอนนี้ แต่ผ่านการทดสอบความคิด


มากอธิบายรายละเอียดเพิ่มเติมจะได้รับในครั้งนี้คำตอบ


2
Rect กำหนด (b.width) เป็น (b.right-b.left) และ (b.height) เป็น (b.bottom-b.top) ดังนั้นคุณสามารถแทนที่ (b.left + b.width) ด้วย (b.right) และ (b.bottom + b.height) ด้วย (2 * b.bottom-b.top) แต่ฉันคิดว่าคุณต้องการ ( b.bottom-b.top) แทนซึ่งเหมือนกับ (b.height) ฉันคิดว่าคุณถูกต้องเกี่ยวกับความกว้าง (ขึ้นอยู่กับการตรวจสอบแหล่งที่มาของ C ++), getTextWidth () ส่งคืนค่าเดียวกันกับ (b.right) อาจมีข้อผิดพลาดในการปัดเศษ (b คือการอ้างอิงถึงขอบเขต)
Nulano

11

ขออภัยที่ต้องตอบคำถามนี้อีกครั้ง ... ฉันต้องการฝังภาพ

ฉันคิดว่าผลลัพธ์ที่ @mice พบนั้นเป็นขีปนาวุธ การสังเกตอาจจะถูกต้องสำหรับขนาดตัวอักษร 60 แต่พวกเขาเปลี่ยนไปมากขึ้นเมื่อข้อความมีขนาดเล็กลง เช่น. 10px ในกรณีนั้นข้อความจะถูกวาดจริงเกินขอบเขต

ป้อนคำอธิบายรูปภาพที่นี่

รหัสที่มาของภาพหน้าจอ:

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

อ่านายกำลังลากอยู่ ฉันยังสนใจในสิ่งนี้แม้ว่าจะไม่สนใจ แจ้งให้เราทราบหากคุณพบสิ่งอื่นโปรด!
slezica

ปัญหาสามารถแก้ไขได้เพียงเล็กน้อยเมื่อคุณคูณฟอนต์ที่คุณต้องการวัดด้วยปัจจัยที่บอกว่า 20 และแบ่งผลลัพธ์ออกจากกัน กว่าคุณอาจต้องการเพิ่มความกว้างของขนาดที่วัดได้อีก 1-2% เพื่อความปลอดภัย ในที่สุดคุณจะมีข้อความที่วัดได้เป็นเวลานาน แต่อย่างน้อยก็ไม่มีข้อความซ้อนทับกัน
Moritz

6
ในการวาดข้อความภายใน rect bounding คุณต้องไม่วาด Text ด้วย X = 0.0f เนื่องจาก rect bounding กลับมาเหมือน rect ไม่ใช่ความกว้างและความสูง หากต้องการวาดอย่างถูกต้องคุณควรชดเชยพิกัด X บนค่า "-bounds.left" จากนั้นจะแสดงอย่างถูกต้อง
โรมัน

10

การปฏิเสธความรับผิด:วิธีนี้ไม่ถูกต้อง 100% ในแง่ของการกำหนดความกว้างน้อยที่สุด

ฉันยังหาวิธีการวัดข้อความบนผืนผ้าใบ หลังจากอ่านโพสต์ยอดเยี่ยมจากหนูฉันมีปัญหาบางอย่างเกี่ยวกับวิธีการวัดข้อความหลายบรรทัด ไม่มีวิธีที่ชัดเจนจากการมีส่วนร่วมเหล่านี้ แต่หลังจากการวิจัยบางอย่างฉัน CAM ทั่วชั้น StaticLayout ช่วยให้คุณสามารถวัดข้อความหลายบรรทัด (ข้อความที่มี "\ n") และกำหนดค่าคุณสมบัติเพิ่มเติมของข้อความของคุณผ่านทางโปรแกรมระบายสีที่เกี่ยวข้อง

นี่เป็นตัวอย่างที่แสดงวิธีการวัดข้อความหลายบรรทัด:

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 สามารถระบุได้ว่าคุณต้องการ จำกัด ข้อความหลายบรรทัดของคุณให้มีความกว้างที่แน่นอนหรือไม่

เนื่องจาก StaticLayout.getWidth () ส่งคืน boundedWidth นี้เท่านั้นคุณจึงต้องทำขั้นตอนอื่นเพื่อให้ได้ความกว้างสูงสุดที่ข้อความหลายบรรทัดของคุณต้องการ คุณสามารถกำหนดความกว้างของแต่ละบรรทัดและความกว้างสูงสุดคือความกว้างของเส้นสูงสุดของหลักสูตร:

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

คุณไม่ควรผ่านiเข้าสู่ getLineWidth (คุณเพิ่งผ่าน 0 ทุกครั้ง)
Rich S

2

มีวิธีอื่นในการวัดขอบเขตข้อความอย่างแม่นยำก่อนอื่นคุณควรได้รับเส้นทางสำหรับสีและข้อความปัจจุบัน ในกรณีของคุณควรเป็นดังนี้:

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

หลังจากนั้นคุณสามารถโทร:

mPath.computeBounds(mBoundsPath, true);

ในรหัสของฉันมันมักจะส่งกลับค่าที่ถูกต้องและคาดหวัง แต่ไม่แน่ใจว่ามันทำงานได้เร็วกว่าแนวทางของคุณหรือไม่


ฉันพบว่ามีประโยชน์เนื่องจากฉันทำ getTextPath อยู่แล้วเพื่อวาดเส้นโครงร่างรอบข้อความ
ToolmakerSteve

2

ความแตกต่างระหว่างgetTextBoundsและmeasureTextอธิบายด้วยภาพด้านล่าง

ในระยะสั้น

  1. getTextBoundsคือการได้รับ RECT ของข้อความที่แน่นอน measureTextคือความยาวของข้อความรวมทั้งช่องว่างพิเศษในด้านซ้ายและขวา

  2. หากมีช่องว่างระหว่างข้อความจะถูกวัดเป็นmeasureTextแต่ไม่รวมอยู่ในความยาวของ TextBounds แม้ว่าพิกัดจะเปลี่ยนไป

  3. ข้อความสามารถเอียง (เอียง) ไปทางซ้าย ในกรณีนี้กล่องขอบด้านซ้ายจะเกินด้านนอกการวัดของ textText และความยาวโดยรวมของข้อความที่ถูกผูกไว้จะใหญ่กว่าmeasureText

  4. ข้อความสามารถเอียง (เอียง) ได้ ในกรณีนี้กล่องขอบด้านขวาจะเกินด้านนอกการวัดของข้อความวัดและความยาวโดยรวมของข้อความที่ถูกผูกไว้จะใหญ่กว่าmeasureText

ป้อนคำอธิบายรูปภาพที่นี่


0

นี่คือวิธีที่ฉันคำนวณขนาดที่แท้จริงของตัวอักษรตัวแรก (คุณสามารถเปลี่ยนส่วนหัวของวิธีการให้เหมาะกับความต้องการของคุณเช่นการchar[]ใช้งานแทน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;
}

โปรดทราบว่าฉันใช้ TextPaint แทนที่จะเป็นคลาส Paint ดั้งเดิม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.