blob: cbed96caf34807d7b5c9cf38551d4e939d371fdb [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 android.graphics.Paint.FontMetricsInt;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.Layout.Alignment;
import static android.text.Layout.Alignment.*;
import android.text.TextPaint;
import android.text.method.EditorState;
import android.util.Log;
import junit.framework.TestCase;
/**
* Tests StaticLayout vertical metrics behavior.
*
* Requires disabling access checks in the vm since this calls package-private
* APIs.
*
* @Suppress
*/
public class StaticLayoutTest extends TestCase {
private static final int DEFAULT_OUTER_WIDTH = 150;
private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER;
private static final float SPACE_MULTI = 1.0f;
private static final float SPACE_ADD = 0.0f;
/**
* Basic test showing expected behavior and relationship between font
* metrics and line metrics.
*/
//@SmallTest
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.
*/
//@SmallTest
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.
*/
//@SmallTest
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.
*/
//@SmallTest
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.
*/
//@SmallTest
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.
*/
//@SmallTest
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 + spacingAdd);
}
/**
* Basic test showing effect of includePad = true, spacingAdd = 2,
* spacingMult = 1.5, wrapping to 3 lines.
*/
//@SmallTest
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 + s.scale(fmi.bottom - fmi.ascent));
}
/**
* Basic test showing effect of includePad = true, spacingAdd = 0,
* spacingMult = 0.8 when wrapping to 3 lines.
*/
//@SmallTest
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 + s.scale(fmi.bottom - fmi.ascent));
}
// ----- 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}.
*/
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);
}
}