blob: fb60e38fcbf6070868220b0f3b9dfb994ee26b85 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package android.text;
import static android.text.Layout.Alignment.ALIGN_NORMAL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.graphics.Paint.FontMetricsInt;
import android.os.LocaleList;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.text.Layout.Alignment;
import android.text.method.EditorState;
import android.text.style.LocaleSpan;
import android.util.Log;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Tests StaticLayout vertical metrics behavior.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class StaticLayoutTest {
private static final float SPACE_MULTI = 1.0f;
private static final float SPACE_ADD = 0.0f;
private static final int DEFAULT_OUTER_WIDTH = 150;
private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar"
+ "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong";
private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence";
private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER;
private static final int ELLIPSIZE_WIDTH = 8;
private StaticLayout mDefaultLayout;
private TextPaint mDefaultPaint;
@Before
public void setup() {
mDefaultPaint = new TextPaint();
mDefaultLayout = createDefaultStaticLayout();
}
private StaticLayout createDefaultStaticLayout() {
return new StaticLayout(LAYOUT_TEXT, mDefaultPaint,
DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
}
@Test
public void testBuilder() {
{
// Obtain.
final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
final StaticLayout layout = builder.build();
// Check default value.
assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
layout.getTextDirectionHeuristic());
}
{
// setTextDirection.
final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
builder.setTextDirection(TextDirectionHeuristics.RTL);
final StaticLayout layout = builder.build();
// Always returns TextDirectionHeuristics.FIRSTSTRONG_LTR.
assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
layout.getTextDirectionHeuristic());
}
}
/**
* Basic test showing expected behavior and relationship between font
* metrics and line metrics.
*/
@Test
public void testGetters1() {
LayoutBuilder b = builder();
FontMetricsInt fmi = b.paint.getFontMetricsInt();
// check default paint
Log.i("TG1:paint", fmi.toString());
Layout l = b.build();
assertVertMetrics(l, 0, 0,
fmi.ascent, fmi.descent);
// other quick metrics
assertEquals(0, l.getLineStart(0));
assertEquals(Layout.DIR_LEFT_TO_RIGHT, l.getParagraphDirection(0));
assertEquals(false, l.getLineContainsTab(0));
assertEquals(Layout.DIRS_ALL_LEFT_TO_RIGHT, l.getLineDirections(0));
assertEquals(0, l.getEllipsisCount(0));
assertEquals(0, l.getEllipsisStart(0));
assertEquals(b.width, l.getEllipsizedWidth());
}
/**
* Basic test showing effect of includePad = true with 1 line.
* Top and bottom padding are affected, as is the line descent and height.
*/
@Test
public void testGetters2() {
LayoutBuilder b = builder()
.setIncludePad(true);
FontMetricsInt fmi = b.paint.getFontMetricsInt();
Layout l = b.build();
assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
fmi.top, fmi.bottom);
}
/**
* Basic test showing effect of includePad = true wrapping to 2 lines.
* Ascent of top line and descent of bottom line are affected.
*/
@Test
public void testGetters3() {
LayoutBuilder b = builder()
.setIncludePad(true)
.setWidth(50);
FontMetricsInt fmi = b.paint.getFontMetricsInt();
Layout l = b.build();
assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
fmi.top, fmi.descent,
fmi.ascent, fmi.bottom);
}
/**
* Basic test showing effect of includePad = true wrapping to 3 lines.
* First line ascent is top, bottom line descent is bottom.
*/
@Test
public void testGetters4() {
LayoutBuilder b = builder()
.setText("This is a longer test")
.setIncludePad(true)
.setWidth(50);
FontMetricsInt fmi = b.paint.getFontMetricsInt();
Layout l = b.build();
assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
fmi.top, fmi.descent,
fmi.ascent, fmi.descent,
fmi.ascent, fmi.bottom);
}
/**
* Basic test showing effect of includePad = true wrapping to 3 lines and
* large text. See effect of leading. Currently, we don't expect there to
* even be non-zero leading.
*/
@Test
public void testGetters5() {
LayoutBuilder b = builder()
.setText("This is a longer test")
.setIncludePad(true)
.setWidth(150);
b.paint.setTextSize(36);
FontMetricsInt fmi = b.paint.getFontMetricsInt();
if (fmi.leading == 0) { // nothing to test
Log.i("TG5", "leading is 0, skipping test");
return;
}
// So far, leading is not used, so this is the same as TG4. If we start
// using leading, this will fail.
Layout l = b.build();
assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
fmi.top, fmi.descent,
fmi.ascent, fmi.descent,
fmi.ascent, fmi.bottom);
}
/**
* Basic test showing effect of includePad = true, spacingAdd = 2, wrapping
* to 3 lines.
*/
@Test
public void testGetters6() {
int spacingAdd = 2; // int so expressions return int
LayoutBuilder b = builder()
.setText("This is a longer test")
.setIncludePad(true)
.setWidth(50)
.setSpacingAdd(spacingAdd);
FontMetricsInt fmi = b.paint.getFontMetricsInt();
Layout l = b.build();
assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
fmi.top, fmi.descent + spacingAdd,
fmi.ascent, fmi.descent + spacingAdd,
fmi.ascent, fmi.bottom);
}
/**
* Basic test showing effect of includePad = true, spacingAdd = 2,
* spacingMult = 1.5, wrapping to 3 lines.
*/
@Test
public void testGetters7() {
LayoutBuilder b = builder()
.setText("This is a longer test")
.setIncludePad(true)
.setWidth(50)
.setSpacingAdd(2)
.setSpacingMult(1.5f);
FontMetricsInt fmi = b.paint.getFontMetricsInt();
Scaler s = new Scaler(b.spacingMult, b.spacingAdd);
Layout l = b.build();
assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
fmi.ascent, fmi.bottom);
}
/**
* Basic test showing effect of includePad = true, spacingAdd = 0,
* spacingMult = 0.8 when wrapping to 3 lines.
*/
@Test
public void testGetters8() {
LayoutBuilder b = builder()
.setText("This is a longer test")
.setIncludePad(true)
.setWidth(50)
.setSpacingAdd(2)
.setSpacingMult(.8f);
FontMetricsInt fmi = b.paint.getFontMetricsInt();
Scaler s = new Scaler(b.spacingMult, b.spacingAdd);
Layout l = b.build();
assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
fmi.ascent, fmi.bottom);
}
// ----- test utility classes and methods -----
// Models the effect of the scale and add parameters. I think the current
// implementation misbehaves.
private static class Scaler {
private final float sMult;
private final float sAdd;
Scaler(float sMult, float sAdd) {
this.sMult = sMult - 1;
this.sAdd = sAdd;
}
public int scale(float height) {
int altVal = (int)(height * sMult + sAdd + 0.5);
int rndVal = Math.round(height * sMult + sAdd);
if (altVal != rndVal) {
Log.i("Scale", "expected scale: " + rndVal +
" != returned scale: " + altVal);
}
return rndVal;
}
}
/* package */ static LayoutBuilder builder() {
return new LayoutBuilder();
}
/* package */ static class LayoutBuilder {
String text = "This is a test";
TextPaint paint = new TextPaint(); // default
int width = 100;
Alignment align = ALIGN_NORMAL;
float spacingMult = 1;
float spacingAdd = 0;
boolean includePad = false;
LayoutBuilder setText(String text) {
this.text = text;
return this;
}
LayoutBuilder setPaint(TextPaint paint) {
this.paint = paint;
return this;
}
LayoutBuilder setWidth(int width) {
this.width = width;
return this;
}
LayoutBuilder setAlignment(Alignment align) {
this.align = align;
return this;
}
LayoutBuilder setSpacingMult(float spacingMult) {
this.spacingMult = spacingMult;
return this;
}
LayoutBuilder setSpacingAdd(float spacingAdd) {
this.spacingAdd = spacingAdd;
return this;
}
LayoutBuilder setIncludePad(boolean includePad) {
this.includePad = includePad;
return this;
}
Layout build() {
return new StaticLayout(text, paint, width, align, spacingMult,
spacingAdd, includePad);
}
}
private void assertVertMetrics(Layout l, int topPad, int botPad, int... values) {
assertTopBotPadding(l, topPad, botPad);
assertLinesMetrics(l, values);
}
private void assertLinesMetrics(Layout l, int... values) {
// sanity check
if ((values.length & 0x1) != 0) {
throw new IllegalArgumentException(String.valueOf(values.length));
}
int lines = values.length >> 1;
assertEquals(lines, l.getLineCount());
int t = 0;
for (int i = 0, n = 0; i < lines; ++i, n += 2) {
int a = values[n];
int d = values[n+1];
int h = -a + d;
assertLineMetrics(l, i, t, a, d, h);
t += h;
}
assertEquals(t, l.getHeight());
}
private void assertLineMetrics(Layout l, int line,
int top, int ascent, int descent, int height) {
String info = "line " + line;
assertEquals(info, top, l.getLineTop(line));
assertEquals(info, ascent, l.getLineAscent(line));
assertEquals(info, descent, l.getLineDescent(line));
assertEquals(info, height, l.getLineBottom(line) - top);
}
private void assertTopBotPadding(Layout l, int topPad, int botPad) {
assertEquals(topPad, l.getTopPadding());
assertEquals(botPad, l.getBottomPadding());
}
private void moveCursorToRightCursorableOffset(EditorState state, TextPaint paint) {
assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart);
state.mSelectionStart = state.mSelectionEnd = newOffset;
}
private void moveCursorToLeftCursorableOffset(EditorState state, TextPaint paint) {
assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
final Layout layout = builder().setText(state.mText.toString()).setPaint(paint).build();
final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart);
state.mSelectionStart = state.mSelectionEnd = newOffset;
}
/**
* Tests for keycap, variation selectors, flags are in CTS.
* See {@link android.text.cts.StaticLayoutTest}.
*/
@Test
public void testEmojiOffset() {
EditorState state = new EditorState();
TextPaint paint = new TextPaint();
// Odd numbered regional indicator symbols.
// U+1F1E6 is REGIONAL INDICATOR SYMBOL LETTER A, U+1F1E8 is REGIONAL INDICATOR SYMBOL
// LETTER C.
state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 |");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6");
moveCursorToLeftCursorableOffset(state, paint);
// Zero width sequence
final String zwjSequence = "U+1F468 U+200D U+2764 U+FE0F U+200D U+1F468";
state.setByString("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
moveCursorToRightCursorableOffset(state, paint);
state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
moveCursorToRightCursorableOffset(state, paint);
state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
moveCursorToRightCursorableOffset(state, paint);
state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
moveCursorToRightCursorableOffset(state, paint);
state.assertEquals(zwjSequence + " " + zwjSequence + " " + zwjSequence + " |");
moveCursorToLeftCursorableOffset(state, paint);
state.assertEquals(zwjSequence + " " + zwjSequence + " | " + zwjSequence);
moveCursorToLeftCursorableOffset(state, paint);
state.assertEquals(zwjSequence + " | " + zwjSequence + " " + zwjSequence);
moveCursorToLeftCursorableOffset(state, paint);
state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
moveCursorToLeftCursorableOffset(state, paint);
state.assertEquals("| " + zwjSequence + " " + zwjSequence + " " + zwjSequence);
moveCursorToLeftCursorableOffset(state, paint);
// Emoji modifiers
// U+261D is WHITE UP POINTING INDEX, U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2.
state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
moveCursorToRightCursorableOffset(state, paint);
state.setByString("U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB |");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("U+261D U+1F3FB U+261D U+1F3FB | U+261D U+1F3FB");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("U+261D U+1F3FB | U+261D U+1F3FB U+261D U+1F3FB");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
moveCursorToLeftCursorableOffset(state, paint);
state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB");
moveCursorToLeftCursorableOffset(state, paint);
}
private StaticLayout createEllipsizeStaticLayout(CharSequence text,
TextUtils.TruncateAt ellipsize, int maxLines) {
return new StaticLayout(text, 0, text.length(),
mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN,
TextDirectionHeuristics.FIRSTSTRONG_LTR,
SPACE_MULTI, SPACE_ADD, true /* include pad */,
ellipsize,
ELLIPSIZE_WIDTH,
maxLines);
}
@Test
public void testEllipsis_singleLine() {
{
// Single line case and TruncateAt.END so that we have some ellipsis
StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
TextUtils.TruncateAt.END, 1);
assertTrue(layout.getEllipsisCount(0) > 0);
}
{
// Single line case and TruncateAt.MIDDLE so that we have some ellipsis
StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
TextUtils.TruncateAt.MIDDLE, 1);
assertTrue(layout.getEllipsisCount(0) > 0);
}
{
// Single line case and TruncateAt.END so that we have some ellipsis
StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
TextUtils.TruncateAt.END, 1);
assertTrue(layout.getEllipsisCount(0) > 0);
}
{
// Single line case and TruncateAt.MARQUEE so that we have NO ellipsis
StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE,
TextUtils.TruncateAt.MARQUEE, 1);
assertTrue(layout.getEllipsisCount(0) == 0);
}
{
final String text = "\u3042" // HIRAGANA LETTER A
+ "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
final float textWidth = mDefaultPaint.measureText(text);
final int halfWidth = (int) (textWidth / 2.0f);
{
StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1);
assertTrue(layout.getEllipsisCount(0) > 0);
assertTrue(layout.getEllipsisStart(0) > 0);
}
{
StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.START, halfWidth, 1);
assertTrue(layout.getEllipsisCount(0) > 0);
assertEquals(0, mDefaultLayout.getEllipsisStart(0));
}
{
StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MIDDLE, halfWidth, 1);
assertTrue(layout.getEllipsisCount(0) > 0);
assertTrue(layout.getEllipsisStart(0) > 0);
}
{
StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MARQUEE, halfWidth, 1);
assertEquals(0, layout.getEllipsisCount(0));
}
}
{
// The white spaces in this text will be trailing if maxLines is larger than 1, but
// width of the trailing white spaces must not be ignored if ellipsis is applied.
final String text = "abc def";
final float textWidth = mDefaultPaint.measureText(text);
final int halfWidth = (int) (textWidth / 2.0f);
{
StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1);
assertTrue(layout.getEllipsisCount(0) > 0);
assertTrue(layout.getEllipsisStart(0) > 0);
}
}
{
// 2 family emojis (11 code units + 11 code units).
final String text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"
+ "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66";
final float textWidth = mDefaultPaint.measureText(text);
final TextUtils.TruncateAt[] kinds = {TextUtils.TruncateAt.START,
TextUtils.TruncateAt.MIDDLE, TextUtils.TruncateAt.END};
for (final TextUtils.TruncateAt kind : kinds) {
for (int i = 0; i <= 8; i++) {
int avail = (int) (textWidth * i / 7.0f);
StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint,
avail, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR,
SPACE_MULTI, SPACE_ADD, false, kind, avail, 1);
assertTrue(layout.getEllipsisCount(0) == text.length()
|| layout.getEllipsisCount(0) == text.length() / 2
|| layout.getEllipsisCount(0) == 0);
}
}
}
}
// String wrapper for testing not well known implementation of CharSequence.
private class FakeCharSequence implements CharSequence {
private String mStr;
FakeCharSequence(String str) {
mStr = str;
}
@Override
public char charAt(int index) {
return mStr.charAt(index);
}
@Override
public int length() {
return mStr.length();
}
@Override
public CharSequence subSequence(int start, int end) {
return mStr.subSequence(start, end);
}
@Override
public String toString() {
return mStr;
}
};
private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) {
List<CharSequence> result = new ArrayList<>();
List<String> normalizedStrings = new ArrayList<>();
for (Normalizer.Form form: forms) {
normalizedStrings.add(Normalizer.normalize(testString, form));
}
for (String str: normalizedStrings) {
result.add(str);
result.add(new SpannedString(str));
result.add(new SpannableString(str));
result.add(new SpannableStringBuilder(str)); // as a GraphicsOperations implementation.
result.add(new FakeCharSequence(str)); // as a not well known implementation.
}
return result;
}
private String buildTestMessage(CharSequence seq) {
String normalized;
if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) {
normalized = "NFC";
} else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) {
normalized = "NFD";
} else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) {
normalized = "NFKC";
} else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) {
normalized = "NFKD";
} else {
throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD");
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < seq.length(); ++i) {
builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i))));
}
return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]"
+ ", class: " + seq.getClass().getName()
+ ", Normalization: " + normalized;
}
@Test
public void testGetOffset_UNICODE_Hebrew() {
String testString = "\u05DE\u05E1\u05E2\u05D3\u05D4"; // Hebrew Characters
for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN,
TextDirectionHeuristics.RTL, SPACE_MULTI, SPACE_ADD, true);
String testLabel = buildTestMessage(seq);
assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0));
assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
assertEquals(testLabel, 3, layout.getOffsetToLeftOf(2));
assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
assertEquals(testLabel, 5, layout.getOffsetToLeftOf(4));
assertEquals(testLabel, 5, layout.getOffsetToLeftOf(5));
assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
assertEquals(testLabel, 1, layout.getOffsetToRightOf(2));
assertEquals(testLabel, 2, layout.getOffsetToRightOf(3));
assertEquals(testLabel, 3, layout.getOffsetToRightOf(4));
assertEquals(testLabel, 4, layout.getOffsetToRightOf(5));
}
}
@Test
public void testLocaleSpanAffectsHyphenation() {
TextPaint paint = new TextPaint();
paint.setTextLocale(Locale.US);
// Private use language, with no hyphenation rules.
final Locale privateLocale = Locale.forLanguageTag("qaa");
final String longWord = "philanthropic";
final float wordWidth = paint.measureText(longWord);
// Wide enough that words get hyphenated by default.
final int paraWidth = Math.round(wordWidth * 1.8f);
final String sentence = longWord + " " + longWord + " " + longWord + " " + longWord + " "
+ longWord + " " + longWord;
final int numEnglishLines = StaticLayout.Builder
.obtain(sentence, 0, sentence.length(), paint, paraWidth)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
.build()
.getLineCount();
{
final SpannableString text = new SpannableString(sentence);
text.setSpan(new LocaleSpan(privateLocale), 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
final int numPrivateLocaleLines = StaticLayout.Builder
.obtain(text, 0, text.length(), paint, paraWidth)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
.build()
.getLineCount();
// Since the paragraph set to English gets hyphenated, the number of lines would be
// smaller than the number of lines when there is a span setting a language that
// doesn't get hyphenated.
assertTrue(numEnglishLines < numPrivateLocaleLines);
}
{
// Same as the above test, except that the locale span now uses a locale list starting
// with the private non-hyphenating locale.
final SpannableString text = new SpannableString(sentence);
final LocaleList locales = new LocaleList(privateLocale, Locale.US);
text.setSpan(new LocaleSpan(locales), 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
final int numPrivateLocaleLines = StaticLayout.Builder
.obtain(text, 0, text.length(), paint, paraWidth)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
.build()
.getLineCount();
assertTrue(numEnglishLines < numPrivateLocaleLines);
}
{
final SpannableString text = new SpannableString(sentence);
// Apply the private LocaleSpan only to the first word, which is not getting hyphenated
// anyway.
text.setSpan(new LocaleSpan(privateLocale), 0, longWord.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
final int numPrivateLocaleLines = StaticLayout.Builder
.obtain(text, 0, text.length(), paint, paraWidth)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
.build()
.getLineCount();
// Since the first word is not hyphenated anyway (there's enough width), the LocaleSpan
// should not affect the layout.
assertEquals(numEnglishLines, numPrivateLocaleLines);
}
}
}