Merge "Introduce Layout#fillCharacterBounds"
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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 -->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user