Merge "Introduce Layout#fillCharacterBounds"

This commit is contained in:
Haoyu Zhang
2022-08-02 21:25:01 +00:00
committed by Android (Google) Code Review
6 changed files with 548 additions and 54 deletions

View File

@@ -45091,6 +45091,7 @@ package android.text {
ctor protected Layout(CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float);
method public void draw(android.graphics.Canvas);
method public void draw(android.graphics.Canvas, android.graphics.Path, android.graphics.Paint, int);
method public void fillCharacterBounds(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull float[], @IntRange(from=0) int);
method public final android.text.Layout.Alignment getAlignment();
method public abstract int getBottomPadding();
method public void getCursorPath(int, android.graphics.Path, CharSequence);

View File

@@ -18,6 +18,7 @@ package android.text;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -1311,6 +1312,101 @@ public abstract class Layout {
return horizontal;
}
/**
* Return the characters' bounds in the given range. The {@code bounds} array will be filled
* starting from {@code boundsStart} (inclusive). The coordinates are in local text layout.
*
* @param start the start index to compute the character bounds, inclusive.
* @param end the end index to compute the character bounds, exclusive.
* @param bounds the array to fill in the character bounds. The array is divided into segments
* of four where each index in that segment represents left, top, right and
* bottom of the character.
* @param boundsStart the inclusive start index in the array to start filling in the values
* from.
*
* @throws IndexOutOfBoundsException if the range defined by {@code start} and {@code end}
* exceeds the range of the text, or {@code bounds} doesn't have enough space to store the
* result.
* @throws IllegalArgumentException if {@code bounds} is null.
*/
public void fillCharacterBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
@NonNull float[] bounds, @IntRange(from = 0) int boundsStart) {
if (start < 0 || end < start || end > mText.length()) {
throw new IndexOutOfBoundsException("given range: " + start + ", " + end + " is "
+ "out of the text range: 0, " + mText.length());
}
if (bounds == null) {
throw new IllegalArgumentException("bounds can't be null.");
}
final int neededLength = 4 * (end - start);
if (neededLength > bounds.length - boundsStart) {
throw new IndexOutOfBoundsException("bounds doesn't have enough space to store the "
+ "result, needed: " + neededLength + " had: "
+ (bounds.length - boundsStart));
}
if (start == end) {
return;
}
final int startLine = getLineForOffset(start);
final int endLine = getLineForOffset(end - 1);
float[] horizontalBounds = null;
for (int line = startLine; line <= endLine; ++line) {
final int lineStart = getLineStart(line);
final int lineEnd = getLineEnd(line);
final int lineLength = lineEnd - lineStart;
final int dir = getParagraphDirection(line);
final boolean hasTab = getLineContainsTab(line);
final Directions directions = getLineDirections(line);
TabStops tabStops = null;
if (hasTab && mText instanceof Spanned) {
// Just checking this line should be good enough, tabs should be
// consistent across all lines in a paragraph.
TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, lineStart, lineEnd,
TabStopSpan.class);
if (tabs.length > 0) {
tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
}
}
final TextLine tl = TextLine.obtain();
tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops,
getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
isFallbackLineSpacingEnabled());
if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
horizontalBounds = new float[2 * lineLength];
}
tl.measureAllBounds(horizontalBounds, null);
TextLine.recycle(tl);
final int lineLeft = getParagraphLeft(line);
final int lineRight = getParagraphRight(line);
final int lineStartPos = getLineStartPos(line, lineLeft, lineRight);
final int lineTop = getLineTop(line);
final int lineBottom = getLineBottom(line);
final int startIndex = Math.max(start, lineStart);
final int endIndex = Math.min(end, lineEnd);
for (int index = startIndex; index < endIndex; ++index) {
final int offset = index - lineStart;
final float left = horizontalBounds[offset * 2] + lineStartPos;
final float right = horizontalBounds[offset * 2 + 1] + lineStartPos;
final int boundsIndex = boundsStart + 4 * (index - start);
bounds[boundsIndex] = left;
bounds[boundsIndex + 1] = lineTop;
bounds[boundsIndex + 2] = right;
bounds[boundsIndex + 3] = lineBottom;
}
}
}
/**
* Get the leftmost position that should be exposed for horizontal
* scrolling on the specified line.

View File

@@ -12530,64 +12530,38 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void populateCharacterBounds(CursorAnchorInfo.Builder builder,
int startIndex, int endIndex, float viewportToContentHorizontalOffset,
float viewportToContentVerticalOffset) {
final int minLine = mLayout.getLineForOffset(startIndex);
final int maxLine = mLayout.getLineForOffset(endIndex - 1);
final Rect rect = new Rect();
getLocalVisibleRect(rect);
final RectF visibleRect = new RectF(rect);
for (int line = minLine; line <= maxLine; ++line) {
final int lineStart = mLayout.getLineStart(line);
final int lineEnd = mLayout.getLineEnd(line);
final int offsetStart = Math.max(lineStart, startIndex);
final int offsetEnd = Math.min(lineEnd, endIndex);
final boolean ltrLine =
mLayout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
final float[] widths = new float[offsetEnd - offsetStart];
mLayout.getPaint().getTextWidths(mTransformed, offsetStart, offsetEnd, widths);
final float top = mLayout.getLineTop(line);
final float bottom = mLayout.getLineBottom(line);
for (int offset = offsetStart; offset < offsetEnd; ++offset) {
final float charWidth = widths[offset - offsetStart];
final boolean isRtl = mLayout.isRtlCharAt(offset);
// TODO: This doesn't work perfectly for text with custom styles and
// TAB chars.
final float left;
if (ltrLine) {
if (isRtl) {
left = mLayout.getSecondaryHorizontal(offset) - charWidth;
} else {
left = mLayout.getPrimaryHorizontal(offset);
}
} else {
if (!isRtl) {
left = mLayout.getSecondaryHorizontal(offset);
} else {
left = mLayout.getPrimaryHorizontal(offset) - charWidth;
}
}
final float right = left + charWidth;
// TODO: Check top-right and bottom-left as well.
final float localLeft = left + viewportToContentHorizontalOffset;
final float localRight = right + viewportToContentHorizontalOffset;
final float localTop = top + viewportToContentVerticalOffset;
final float localBottom = bottom + viewportToContentVerticalOffset;
final boolean isTopLeftVisible = visibleRect.contains(localLeft, localTop);
final boolean isBottomRightVisible =
visibleRect.contains(localRight, localBottom);
int characterBoundsFlags = 0;
if (isTopLeftVisible || isBottomRightVisible) {
characterBoundsFlags |= FLAG_HAS_VISIBLE_REGION;
}
if (!isTopLeftVisible || !isBottomRightVisible) {
characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
}
if (isRtl) {
characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
}
// Here offset is the index in Java chars.
builder.addCharacterBounds(offset, localLeft, localTop, localRight,
localBottom, characterBoundsFlags);
final float[] characterBounds = new float[4 * (endIndex - startIndex)];
mLayout.fillCharacterBounds(startIndex, endIndex, characterBounds, 0);
final int limit = endIndex - startIndex;
for (int offset = 0; offset < limit; ++offset) {
final float left =
characterBounds[offset * 4] + viewportToContentHorizontalOffset;
final float top =
characterBounds[offset * 4 + 1] + viewportToContentVerticalOffset;
final float right =
characterBounds[offset * 4 + 2] + viewportToContentHorizontalOffset;
final float bottom =
characterBounds[offset * 4 + 3] + viewportToContentVerticalOffset;
final boolean hasVisibleRegion = visibleRect.intersects(left, top, right, bottom);
final boolean hasInVisibleRegion = !visibleRect.contains(left, top, right, bottom);
int characterBoundsFlags = 0;
if (hasVisibleRegion) {
characterBoundsFlags |= FLAG_HAS_VISIBLE_REGION;
}
if (hasInVisibleRegion) {
characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
}
if (mLayout.isRtlCharAt(offset)) {
characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
}
builder.addCharacterBounds(offset + startIndex, left, top, right, bottom,
characterBoundsFlags);
}
}

View File

@@ -144,6 +144,7 @@
<map code="0x0056" name="5em" /> <!-- V -->
<map code="0x0058" name="10em" /> <!-- X -->
<map code="0x005f" name="0em" /> <!-- _ -->
<map code="0x000a" name="0em" /> <!-- NEW_LINE -->
<map code="0x05D0" name="1em" /> <!-- HEBREW LETTER ALEF -->
<map code="0x05D1" name="5em" /> <!-- HEBREW LETTER BET -->
<map code="0xfffd" name="7em" /> <!-- REPLACEMENT CHAR -->

View File

@@ -0,0 +1,422 @@
/*
* Copyright (C) 2022 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.widget;
import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
import static android.view.inputmethod.CursorAnchorInfo.FLAG_IS_RTL;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import android.app.Activity;
import android.app.Instrumentation;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.text.Layout;
import android.util.TypedValue;
import android.view.View;
import android.view.inputmethod.CursorAnchorInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class TextViewPopulateCharacterBoundsTest {
@Rule
public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>(
TextViewActivity.class);
private Activity mActivity;
private Instrumentation mInstrumentation;
private Typeface mTypeface;
@Before
public void setup() {
mActivity = mActivityRule.getActivity();
mInstrumentation = InstrumentationRegistry.getInstrumentation();
// The test font has following coverage and width.
// U+0020: 10em
// U+002E (.): 10em
// U+0043 (C): 100em
// U+0049 (I): 1em
// U+004C (L): 50em
// U+0056 (V): 5em
// U+0058 (X): 10em
// U+005F (_): 0em
// U+05D0 : 1em // HEBREW LETTER ALEF
// U+05D1 : 5em // HEBREW LETTER BET
// U+FFFD (invalid surrogate will be replaced to this): 7em
// U+10331 (\uD800\uDF31): 10em
// Undefined : 0.5em
mTypeface = Typeface.createFromAsset(mInstrumentation.getTargetContext().getAssets(),
"fonts/StaticLayoutLineBreakingTestFont.ttf");
}
private TextView createTextView(String text, float textSize, int width, int height) {
final TextView textView = new TextView(mActivity);
textView.setTypeface(mTypeface);
textView.setText(text);
// Make 1 em equal to 10 pixels.
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
textView.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
textView.layout(0, 0, width, height);
return textView;
}
@Test
public void testPopulateCharacterBounds_LTR() {
final String text = "IIVX";
final TextView textView = createTextView(text, 10.0f, 200, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(0.0f, layout.getLineTop(0), 10.0f, layout.getLineBottom(0)),
new RectF(10.0f, layout.getLineTop(0), 20.0f, layout.getLineBottom(0)),
new RectF(20.0f, layout.getLineTop(0), 70.0f, layout.getLineBottom(0)),
new RectF(70.0f, layout.getLineTop(0), 170.0f, layout.getLineBottom(0))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_LTR_multiline() {
final String text = "IVVI";
final TextView textView = createTextView(text, 10.0f, 100, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(0.0f, layout.getLineTop(0), 10.0f, layout.getLineBottom(0)),
new RectF(10.0f, layout.getLineTop(0), 60.0f, layout.getLineBottom(0)),
// The second line.
new RectF(0.0f, layout.getLineTop(1), 50.0f, layout.getLineBottom(1)),
new RectF(50.0f, layout.getLineTop(1), 60.0f, layout.getLineBottom(1))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_LTR_newline() {
final String text = "IV\nVI";
final TextView textView = createTextView(text, 10.0f, 100, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(0.0f, layout.getLineTop(0), 10.0f, layout.getLineBottom(0)),
new RectF(10.0f, layout.getLineTop(0), 60.0f, layout.getLineBottom(0)),
// Newline belongs to the first line, and it has 0 width in the font.
new RectF(60.0f, layout.getLineTop(0), 60.0f, layout.getLineBottom(0)),
// The second line.
new RectF(0.0f, layout.getLineTop(1), 50.0f, layout.getLineBottom(1)),
new RectF(50.0f, layout.getLineTop(1), 60.0f, layout.getLineBottom(1))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_RTL() {
final String text = "\u05D0\u05D0\u05D1\u05D1";
final TextView textView = createTextView(text, 10.0f, 200, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(190.0f, layout.getLineTop(0), 200.0f, layout.getLineBottom(0)),
new RectF(180.0f, layout.getLineTop(0), 190.0f, layout.getLineBottom(0)),
new RectF(130.0f, layout.getLineTop(0), 180.0f, layout.getLineBottom(0)),
new RectF(80.0f, layout.getLineTop(0), 130.0f, layout.getLineBottom(0))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_RTL_multiline() {
final String text = "\u05D0\u05D1\u05D1\u05D0";
final TextView textView = createTextView(text, 10.0f, 100, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(90.0f, layout.getLineTop(0), 100.0f, layout.getLineBottom(0)),
new RectF(40.0f, layout.getLineTop(0), 90.0f, layout.getLineBottom(0)),
// The second line
new RectF(50.0f, layout.getLineTop(1), 100.0f, layout.getLineBottom(1)),
new RectF(40.0f, layout.getLineTop(1), 50.0f, layout.getLineBottom(1))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_RTL_newline() {
final String text = "\u05D0\u05D1\n\u05D1\u05D0";
final TextView textView = createTextView(text, 10.0f, 100, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(90.0f, layout.getLineTop(0), 100.0f, layout.getLineBottom(0)),
new RectF(40.0f, layout.getLineTop(0), 90.0f, layout.getLineBottom(0)),
// Newline belongs to the first line, and it has 0 width in the font.
new RectF(40.0f, layout.getLineTop(0), 40.0f, layout.getLineBottom(0)),
// The second line
new RectF(50.0f, layout.getLineTop(1), 100.0f, layout.getLineBottom(1)),
new RectF(40.0f, layout.getLineTop(1), 50.0f, layout.getLineBottom(1))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
// Newline is in an RTL run.
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_BiDi() {
final String text = "IV\u05D0\u05D1IV";
final TextView textView = createTextView(text, 10.0f, 200, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(0.0f, layout.getLineTop(0), 10.0f, layout.getLineBottom(0)),
new RectF(10.0f, layout.getLineTop(0), 60.0f, layout.getLineBottom(0)),
new RectF(110.0f, layout.getLineTop(0), 120.0f, layout.getLineBottom(0)),
new RectF(60.0f, layout.getLineTop(0), 110.0f, layout.getLineBottom(0)),
new RectF(120.0f, layout.getLineTop(0), 130.0f, layout.getLineBottom(0)),
new RectF(130.0f, layout.getLineTop(0), 180.0f, layout.getLineBottom(0))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_BiDi_multiline() {
final String text = "IV\u05D0\u05D1IV";
final TextView textView = createTextView(text, 10.0f, 100, 1000);
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
textView.populateCharacterBounds(builder, 0, text.length(), 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final Layout layout = textView.getLayout();
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(0.0f, layout.getLineTop(0), 10.0f, layout.getLineBottom(0)),
new RectF(10.0f, layout.getLineTop(0), 60.0f, layout.getLineBottom(0)),
new RectF(60.0f, layout.getLineTop(0), 70.0f, layout.getLineBottom(0)),
// The second line.
new RectF(0.0f, layout.getLineTop(1), 50.0f, layout.getLineBottom(1)),
new RectF(50.0f, layout.getLineTop(1), 60.0f, layout.getLineBottom(1)),
// The third line
new RectF(0.0f, layout.getLineTop(2), 50.0f, layout.getLineBottom(2))
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL,
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_charactersWithInvisibleRegion() {
final String text = "IVVI";
final TextView textView = createTextView(text, 10.0f, 100, 1000);
final Layout layout = textView.getLayout();
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
final int verticalOffset = -50;
// Make viewToContentVerticalOffset -50px to simulate the case where TextView is scrolled.
textView.populateCharacterBounds(builder, 0, text.length(), 0, verticalOffset);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
final float firstLineTop = layout.getLineTop(0) + verticalOffset;
final float firstLineBottom = layout.getLineBottom(0) + verticalOffset;
final float secondLineTop = layout.getLineTop(1) + verticalOffset;
final float secondLineBottom = layout.getLineBottom(1) + verticalOffset;
final RectF[] expectedCharacterBounds = new RectF[] {
new RectF(0.0f, firstLineTop, 10.0f, firstLineBottom),
new RectF(10.0f, firstLineTop, 60.0f, firstLineBottom),
new RectF(0.0f, secondLineTop, 50.0f, secondLineBottom),
new RectF(50.0f, secondLineTop, 60.0f, secondLineBottom)
};
assertCharacterBounds(expectedCharacterBounds, cursorAnchorInfo);
final int[] expectedCharacterBoundsFlags = new int[] {
FLAG_HAS_VISIBLE_REGION | FLAG_HAS_INVISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION | FLAG_HAS_INVISIBLE_REGION,
// The second line is visible.
FLAG_HAS_VISIBLE_REGION,
FLAG_HAS_VISIBLE_REGION
};
assertCharacterBoundsFlags(expectedCharacterBoundsFlags, cursorAnchorInfo);
}
@Test
public void testPopulateCharacterBounds_withinRange() {
final String text = "IVVI";
final TextView textView = createTextView(text, 10.0f, 100, 1000);
final Layout layout = textView.getLayout();
final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
builder.setMatrix(Matrix.IDENTITY_MATRIX);
// Only query for character bounds within the range [2, 4).
textView.populateCharacterBounds(builder, 2, 4, 0, 0);
final CursorAnchorInfo cursorAnchorInfo = builder.build();
assertThat(cursorAnchorInfo.getCharacterBounds(2)).isEqualTo(
new RectF(0.0f, layout.getLineTop(1), 50.0f, layout.getLineBottom(1)));
assertThat(cursorAnchorInfo.getCharacterBounds(3)).isEqualTo(
new RectF(50.0f, layout.getLineTop(1), 60.0f, layout.getLineBottom(1)));
assertThat(cursorAnchorInfo.getCharacterBoundsFlags(2)).isEqualTo(FLAG_HAS_VISIBLE_REGION);
assertThat(cursorAnchorInfo.getCharacterBoundsFlags(3)).isEqualTo(FLAG_HAS_VISIBLE_REGION);
}
private static void assertCharacterBounds(RectF[] expected,
CursorAnchorInfo cursorAnchorInfo) {
final RectF[] characterBounds = new RectF[expected.length];
for (int i = 0; i < expected.length; ++i) {
characterBounds[i] = cursorAnchorInfo.getCharacterBounds(i);
}
assertArrayEquals(expected, characterBounds);
}
private static void assertCharacterBoundsFlags(int[] expected,
CursorAnchorInfo cursorAnchorInfo) {
final int[] characterBoundsFlags = new int[expected.length];
for (int i = 0; i < expected.length; ++i) {
characterBoundsFlags[i] = cursorAnchorInfo.getCharacterBoundsFlags(i);
}
assertArrayEquals(expected, characterBoundsFlags);
}
}