script-astra/Android/Sdk/sources/android-35/android/view/inputmethod/TextBoundsInfo.java
localadmin 4380f00a78 init
2025-01-20 18:15:20 +03:00

1413 lines
62 KiB
Java

/*
* 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.view.inputmethod;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Layout;
import android.text.SegmentFinder;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* The text bounds information of a slice of text in the editor.
*
* <p> This class provides IME the layout information of the text within the range from
* {@link #getStartIndex()} to {@link #getEndIndex()}. It's intended to be used by IME as a
* supplementary API to support handwriting gestures.
* </p>
*/
public final class TextBoundsInfo implements Parcelable {
/**
* The flag indicating that the character is a whitespace.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_CHARACTER_WHITESPACE = 1;
/**
* The flag indicating that the character is a linefeed character.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_CHARACTER_LINEFEED = 1 << 1;
/**
* The flag indicating that the character is a punctuation.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_CHARACTER_PUNCTUATION = 1 << 2;
/**
* The flag indicating that the line this character belongs to has RTL line direction. It's
* required that all characters in the same line must have the same direction.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_LINE_IS_RTL = 1 << 3;
/** @hide */
@IntDef(prefix = "FLAG_", flag = true, value = {
FLAG_CHARACTER_WHITESPACE,
FLAG_CHARACTER_LINEFEED,
FLAG_CHARACTER_PUNCTUATION,
FLAG_LINE_IS_RTL
})
@Retention(RetentionPolicy.SOURCE)
public @interface CharacterFlags {}
/** All the valid flags. */
private static final int KNOWN_CHARACTER_FLAGS = FLAG_CHARACTER_WHITESPACE
| FLAG_CHARACTER_LINEFEED | FLAG_CHARACTER_PUNCTUATION | FLAG_LINE_IS_RTL;
/**
* The amount of shift to get the character's BiDi level from the internal character flags.
*/
private static final int BIDI_LEVEL_SHIFT = 19;
/**
* The mask used to get the character's BiDi level from the internal character flags.
*/
private static final int BIDI_LEVEL_MASK = 0x7F << BIDI_LEVEL_SHIFT;
/**
* The flag indicating that the character at the index is the start of a line segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_LINE_SEGMENT_START = 1 << 31;
/**
* The flag indicating that the character at the index is the end of a line segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_LINE_SEGMENT_END = 1 << 30;
/**
* The flag indicating that the character at the index is the start of a word segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_WORD_SEGMENT_START = 1 << 29;
/**
* The flag indicating that the character at the index is the end of a word segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_WORD_SEGMENT_END = 1 << 28;
/**
* The flag indicating that the character at the index is the start of a grapheme segment.
* It's only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_GRAPHEME_SEGMENT_START = 1 << 27;
/**
* The flag indicating that the character at the index is the end of a grapheme segment.
* It's only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_GRAPHEME_SEGMENT_END = 1 << 26;
private final int mStart;
private final int mEnd;
private final float[] mMatrixValues;
private final float[] mCharacterBounds;
/**
* The array that encodes character and BiDi levels. They are stored together to save memory
* space, and it's easier during serialization.
*/
private final int[] mInternalCharacterFlags;
private final SegmentFinder mLineSegmentFinder;
private final SegmentFinder mWordSegmentFinder;
private final SegmentFinder mGraphemeSegmentFinder;
/**
* Set the given {@link android.graphics.Matrix} to be the transformation
* matrix that is to be applied other positional data in this class.
*/
@NonNull
public void getMatrix(@NonNull Matrix matrix) {
Objects.requireNonNull(matrix);
matrix.setValues(mMatrixValues);
}
/**
* Returns the index of the first character whose bounds information is available in this
* {@link TextBoundsInfo}, inclusive.
*
* @see Builder#setStartAndEnd(int, int)
*/
public int getStartIndex() {
return mStart;
}
/**
* Returns the index of the last character whose bounds information is available in this
* {@link TextBoundsInfo}, exclusive.
*
* @see Builder#setStartAndEnd(int, int)
*/
public int getEndIndex() {
return mEnd;
}
/**
* Set the bounds of the character at the given {@code index} to the given {@link RectF}, in
* the coordinates of the editor.
*
* @param index the index of the queried character.
* @param bounds the {@link RectF} used to receive the result.
*
* @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
* the {@code start} to the {@code end}.
*/
@NonNull
public void getCharacterBounds(int index, @NonNull RectF bounds) {
if (index < mStart || index >= mEnd) {
throw new IndexOutOfBoundsException("Index is out of the bounds of "
+ "[" + mStart + ", " + mEnd + ").");
}
final int offset = 4 * (index - mStart);
bounds.set(mCharacterBounds[offset], mCharacterBounds[offset + 1],
mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]);
}
/**
* Return the flags associated with the character at the given {@code index}.
* The flags contain the following information:
* <ul>
* <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
* whitespace. </li>
* <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
* linefeed. </li>
* <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
* punctuation. </li>
* <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
* has RTL line direction. All characters in the same line must have the same line
* direction. Check {@link #getLineSegmentFinder()} for more information of
* line boundaries. </li>
* </ul>
*
* @param index the index of the queried character.
* @return the flags associated with the queried character.
*
* @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
* the {@code start} to the {@code end}.
*
* @see #FLAG_CHARACTER_WHITESPACE
* @see #FLAG_CHARACTER_LINEFEED
* @see #FLAG_CHARACTER_PUNCTUATION
* @see #FLAG_LINE_IS_RTL
*/
@CharacterFlags
public int getCharacterFlags(int index) {
if (index < mStart || index >= mEnd) {
throw new IndexOutOfBoundsException("Index is out of the bounds of "
+ "[" + mStart + ", " + mEnd + ").");
}
final int offset = index - mStart;
return mInternalCharacterFlags[offset] & KNOWN_CHARACTER_FLAGS;
}
/**
* The BiDi level of the character at the given {@code index}. <br/>
* BiDi level is defined by
* <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
* bidirectional algorithm </a>. One can determine whether a character's direction is
* right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
* If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
* character must be in the range of [0, 125].
*
* @param index the index of the queried character.
* @return the BiDi level of the character, which is an integer in the range of [0, 125].
* @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
* the {@code start} to the {@code end}.
*
* @see Builder#setCharacterBidiLevel(int[])
*/
@IntRange(from = 0, to = 125)
public int getCharacterBidiLevel(int index) {
if (index < mStart || index >= mEnd) {
throw new IndexOutOfBoundsException("Index is out of the bounds of "
+ "[" + mStart + ", " + mEnd + ").");
}
final int offset = index - mStart;
return (mInternalCharacterFlags[offset] & BIDI_LEVEL_MASK) >> BIDI_LEVEL_SHIFT;
}
/**
* Returns the {@link SegmentFinder} that locates the word boundaries.
*
* @see Builder#setWordSegmentFinder(SegmentFinder)
*/
@NonNull
public SegmentFinder getWordSegmentFinder() {
return mWordSegmentFinder;
}
/**
* Returns the {@link SegmentFinder} that locates the grapheme boundaries.
*
* @see Builder#setGraphemeSegmentFinder(SegmentFinder)
*/
@NonNull
public SegmentFinder getGraphemeSegmentFinder() {
return mGraphemeSegmentFinder;
}
/**
* Returns the {@link SegmentFinder} that locates the line boundaries.
*
* @see Builder#setLineSegmentFinder(SegmentFinder)
*/
@NonNull
public SegmentFinder getLineSegmentFinder() {
return mLineSegmentFinder;
}
/**
* Return the index of the closest character to the given position.
* It's similar to the text layout API {@link Layout#getOffsetForHorizontal(int, float)}.
* And it's mainly used to find the cursor index (the index of the character before which the
* cursor should be placed) for the given position. It's guaranteed that the returned index is
* a grapheme break. Check {@link #getGraphemeSegmentFinder()} for more information.
*
* <p>It's assumed that the editor lays out text in horizontal lines from top to bottom and each
* line is laid out according to the display algorithm specified in
* <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
* algorithm</a>.
* </p>
*
* <p> This method won't check the text ranges whose line information is missing. For example,
* the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated
* {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
* won't check the text in the ranges of [5, 7) and [12, 15).
* </p>
*
* <p> Under the following conditions, this method will return -1 indicating that no valid
* character is found:
* <ul>
* <li> The given {@code y} coordinate is above the first line or below the last line (the
* first line or the last line is identified by the {@link SegmentFinder} returned from
* {@link #getLineSegmentFinder()}). </li>
* <li> There is no character in this {@link TextBoundsInfo}. </li>
* </ul>
* </p>
*
* @param x the x coordinates of the interested location, in the editor's coordinates.
* @param y the y coordinates of the interested location, in the editor's coordinates.
* @return the index of the character whose position is closest to the given location. It will
* return -1 if it can't find a character.
*
* @see Layout#getOffsetForHorizontal(int, float)
*/
public int getOffsetForPosition(float x, float y) {
final int[] lineRange = new int[2];
final RectF lineBounds = new RectF();
getLineInfo(y, lineRange, lineBounds);
// No line is found, return -1;
if (lineRange[0] == -1 || lineRange[1] == -1) return -1;
final int lineStart = lineRange[0];
final int lineEnd = lineRange[1];
final boolean lineEndsWithLinefeed =
(getCharacterFlags(lineEnd - 1) & FLAG_CHARACTER_LINEFEED) != 0;
// Consider the following 2 cases:
// Case 1:
// Text: "AB\nCD"
// Layout: AB
// CD
// Case 2:
// Text: "ABCD"
// Layout: AB
// CD
// If user wants to insert a 'X' character at the end of the first line:
// In case 1, 'X' is inserted before the last character '\n'.
// In case 2, 'X' is inserted after the last character 'B'.
// So if a line ends with linefeed, it shouldn't check the cursor position after the last
// character.
final int lineLimit;
if (lineEndsWithLinefeed) {
lineLimit = lineEnd;
} else {
lineLimit = lineEnd + 1;
}
// Point graphemeStart to the start of the first grapheme segment intersects with the line.
int graphemeStart = mGraphemeSegmentFinder.nextEndBoundary(lineStart);
// The grapheme information is missing.
if (graphemeStart == SegmentFinder.DONE) return -1;
graphemeStart = mGraphemeSegmentFinder.previousStartBoundary(graphemeStart);
int target = -1;
float minDistance = Float.MAX_VALUE;
while (graphemeStart != SegmentFinder.DONE && graphemeStart < lineLimit) {
if (graphemeStart >= lineStart) {
float cursorPosition = getCursorHorizontalPosition(graphemeStart, lineStart,
lineEnd, lineBounds.left, lineBounds.right);
final float distance = Math.abs(cursorPosition - x);
if (distance < minDistance) {
minDistance = distance;
target = graphemeStart;
}
}
graphemeStart = mGraphemeSegmentFinder.nextStartBoundary(graphemeStart);
}
return target;
}
/**
* Whether the primary position at the given index is the previous character's trailing
* position. <br/>
*
* For LTR character, trailing position is its right edge. For RTL character, trailing position
* is its left edge.
*
* The primary position is defined as the position of a newly inserted character with the
* context direction at the given offset. In contrast, the secondary position is the position
* of a newly inserted character with the context's opposite direction at the given offset.
*
* In Android, the trailing position is used for primary position when the direction run after
* the given index has a higher level than the current direction run.
*
* <p>
* For example:
* (L represents LTR character, and R represents RTL character. The number is the index)
* <pre>
* input text: L0 L1 L2 R3 R4 R5 L6 L7 L8
* render result: L0 L1 L2 R5 R4 R3 L6 L7 L8
* BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ]
* BiDi Level: 0 0 0 1 1 1 0 0 0
* </pre>
*
* The index 3 is a BiDi transition point, the cursor can be placed either after L2 or before
* R3. Because the bidi level of run 1 is higher than the run 0, this method returns true. And
* the cursor should be placed after L2.
* <pre>
* render result: L0 L1 L2 R5 R4 R3 L6 L7 L8
* position after L2: |
* position before R3: |
* result position: |
* </pre>
*
* The index 6 is also a Bidi transition point, the 2 possible cursor positions are exactly the
* same as index 3. However, since the bidi level of run 2 is higher than the run 1, this
* method returns false. And the cursor should be placed before L6.
* <pre>
* render result: L0 L1 L2 R5 R4 R3 L6 L7 L8
* position after R5: |
* position before L6: |
* result position: |
* </pre>
*
* This method helps guarantee that the cursor index and the cursor position forms a one to
* one relation.
* </p>
*
* @param offset the offset of the character in front of which the cursor is placed. It must be
* the start index of a grapheme. And it must be in the range from lineStart to
* lineEnd. An offset equal to lineEnd is allowed. It indicates that the cursor is
* placed at the end of current line instead of the start of the following line.
* @param lineStart the start index of the line that index belongs to, inclusive.
* @param lineEnd the end index of the line that index belongs to, exclusive.
* @return true if primary position is the trailing position of the previous character.
*
* @see #getCursorHorizontalPosition(int, int, int, float, float)
*/
private boolean primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd) {
final int bidiLevel;
if (offset < lineEnd) {
bidiLevel = getCharacterBidiLevel(offset);
} else {
// index equals to lineEnd, use line's BiDi level for the BiDi run.
boolean lineIsRtl =
(getCharacterFlags(offset - 1) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
bidiLevel = lineIsRtl ? 1 : 0;
}
final int bidiLevelBefore;
if (offset > lineStart) {
// Here it assumes index is always the start of a grapheme. And (index - 1) belongs to
// the previous grapheme.
bidiLevelBefore = getCharacterBidiLevel(offset - 1);
} else {
// index equals to lineStart, use line's BiDi level for previous BiDi run.
boolean lineIsRtl =
(getCharacterFlags(offset) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
bidiLevelBefore = lineIsRtl ? 1 : 0;
}
return bidiLevelBefore < bidiLevel;
}
/**
* Returns the x coordinates of the cursor at the given index. (The index of the character
* before which the cursor should be placed.)
*
* @param index the character index before which the cursor is placed. It must be the start
* index of a grapheme. It must be in the range from lineStart to lineEnd.
* An index equal to lineEnd is allowed. It indicates that the cursor is
* placed at the end of current line instead of the start of the following line.
* @param lineStart start index of the line that index belongs to, inclusive.
* @param lineEnd end index of the line that index belongs, exclusive.
* @return the x coordinates of the cursor at the given index,
*
* @see #primaryIsTrailingPrevious(int, int, int)
*/
private float getCursorHorizontalPosition(int index, int lineStart, int lineEnd,
float lineLeft, float lineRight) {
Preconditions.checkArgumentInRange(index, lineStart, lineEnd, "index");
final boolean lineIsRtl = (getCharacterFlags(lineStart) & FLAG_LINE_IS_RTL) != 0;
final boolean isPrimaryIsTrailingPrevious =
primaryIsTrailingPrevious(index, lineStart, lineEnd);
// The index of the character used to compute the cursor position.
final int targetIndex;
// Whether to use the start position of the character.
// For LTR character start is the left edge. For RTL character, start is the right edge.
final boolean isStart;
if (isPrimaryIsTrailingPrevious) {
// (index - 1) belongs to the previous line(if any), return the line start position.
if (index <= lineStart) {
return lineIsRtl ? lineRight : lineLeft;
}
targetIndex = index - 1;
isStart = false;
} else {
// index belongs to the next line(if any), return the line end position.
if (index >= lineEnd) {
return lineIsRtl ? lineLeft : lineRight;
}
targetIndex = index;
isStart = true;
}
// The BiDi level is odd when the character is RTL.
final boolean isRtl = (getCharacterBidiLevel(targetIndex) & 1) != 0;
final int offset = targetIndex - mStart;
// If the character is RTL, the start is the right edge. Otherwise, the start is the
// left edge:
// +-----------------------+
// | | start | end |
// |-------+-------+-------|
// | RTL | right | left |
// |-------+-------+-------|
// | LTR | left | right |
// +-------+-------+-------+
return (isRtl != isStart) ? mCharacterBounds[4 * offset] : mCharacterBounds[4 * offset + 2];
}
/**
* Return the minimal rectangle that contains all the characters in the given range.
*
* @param start the start index of the given range, inclusive.
* @param end the end index of the given range, exclusive.
* @param rectF the {@link RectF} to receive the bounds.
*/
private void getBoundsForRange(int start, int end, @NonNull RectF rectF) {
Preconditions.checkArgumentInRange(start, mStart, mEnd - 1, "start");
Preconditions.checkArgumentInRange(end, start, mEnd, "end");
if (end <= start) {
rectF.setEmpty();
return;
}
rectF.left = Float.MAX_VALUE;
rectF.top = Float.MAX_VALUE;
rectF.right = Float.MIN_VALUE;
rectF.bottom = Float.MIN_VALUE;
for (int index = start; index < end; ++index) {
final int offset = index - mStart;
rectF.left = Math.min(rectF.left, mCharacterBounds[4 * offset]);
rectF.top = Math.min(rectF.top, mCharacterBounds[4 * offset + 1]);
rectF.right = Math.max(rectF.right, mCharacterBounds[4 * offset + 2]);
rectF.bottom = Math.max(rectF.bottom, mCharacterBounds[4 * offset + 3]);
}
}
/**
* Return the character range and bounds of the closest line to the given {@code y} coordinate,
* in the editor's local coordinates.
*
* If the given y is above the first line or below the last line -1 will be returned for line
* start and end.
*
* This method assumes that the lines are laid out from the top to bottom.
*
* @param y the y coordinates used to search for the line.
* @param characterRange a two element array used to receive the character range of the line.
* If no valid line is found -1 will be returned for both start and end.
* @param bounds {@link RectF} to receive the line bounds result, nullable. If given, it can
* still be modified even if no valid line is found.
*/
private void getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds) {
characterRange[0] = -1;
characterRange[1] = -1;
// Starting from the first line.
int currentLineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
if (currentLineEnd == SegmentFinder.DONE) return;
int currentLineStart = mLineSegmentFinder.previousStartBoundary(currentLineEnd);
float top = Float.MAX_VALUE;
float bottom = Float.MIN_VALUE;
float minDistance = Float.MAX_VALUE;
final RectF currentLineBounds = new RectF();
while (currentLineStart != SegmentFinder.DONE && currentLineStart < mEnd) {
final int lineStartInRange = Math.max(mStart, currentLineStart);
final int lineEndInRange = Math.min(mEnd, currentLineEnd);
getBoundsForRange(lineStartInRange, lineEndInRange, currentLineBounds);
top = Math.min(currentLineBounds.top, top);
bottom = Math.max(currentLineBounds.bottom, bottom);
final float distance = verticalDistance(currentLineBounds, y);
if (distance == 0f) {
characterRange[0] = currentLineStart;
characterRange[1] = currentLineEnd;
if (bounds != null) {
bounds.set(currentLineBounds);
}
return;
}
if (distance < minDistance) {
minDistance = distance;
characterRange[0] = currentLineStart;
characterRange[1] = currentLineEnd;
if (bounds != null) {
bounds.set(currentLineBounds);
}
}
if (y < bounds.top) break;
currentLineStart = mLineSegmentFinder.nextStartBoundary(currentLineStart);
currentLineEnd = mLineSegmentFinder.nextEndBoundary(currentLineEnd);
}
// y is above the first line or below the last line. The founded line is still invalid,
// clear the result.
if (y < top || y > bottom) {
characterRange[0] = -1;
characterRange[1] = -1;
if (bounds != null) {
bounds.setEmpty();
}
}
}
/**
* Finds the range of text which is inside the specified rectangle area. This method is a
* counterpart of the
* {@link Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)}.
*
* <p>It's assumed that the editor lays out text in horizontal lines from top to bottom
* and each line is laid out according to the display algorithm specified in
* <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
* algorithm</a>.
* </p>
*
* <p> This method won't check the text ranges whose line information is missing. For example,
* the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated line
* {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
* won't check the text in the ranges of [5, 7) and [12, 15).
* </p>
*
* @param area area for which the text range will be found
* @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
* text segment
* @param inclusionStrategy strategy for determining whether a text segment is inside the
* specified area
* @return the text range stored in a two element int array. The first element is the
* start (inclusive) of the text range, and the second element is the end (exclusive) character
* offsets of the text range, or null if there are no text segments inside the area.
*
* @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)
*/
@Nullable
public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
@NonNull Layout.TextInclusionStrategy inclusionStrategy) {
int lineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
// Line information is missing.
if (lineEnd == SegmentFinder.DONE) return null;
int lineStart = mLineSegmentFinder.previousStartBoundary(lineEnd);
int start = -1;
while (lineStart != SegmentFinder.DONE && start == -1) {
start = getStartForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
inclusionStrategy);
lineStart = mLineSegmentFinder.nextStartBoundary(lineStart);
lineEnd = mLineSegmentFinder.nextEndBoundary(lineEnd);
}
// Can't find the start index; the specified contains no valid segment.
if (start == -1) return null;
lineStart = mLineSegmentFinder.previousStartBoundary(mEnd);
// Line information is missing.
if (lineStart == SegmentFinder.DONE) return null;
lineEnd = mLineSegmentFinder.nextEndBoundary(lineStart);
int end = -1;
while (lineEnd > start && end == -1) {
end = getEndForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
inclusionStrategy);
lineStart = mLineSegmentFinder.previousStartBoundary(lineStart);
lineEnd = mLineSegmentFinder.previousEndBoundary(lineEnd);
}
// We've already found start, end is guaranteed to be found at this point.
start = segmentFinder.previousStartBoundary(start + 1);
end = segmentFinder.nextEndBoundary(end - 1);
return new int[] { start, end };
}
/**
* Find the start character index of the first text segments within a line inside the specified
* {@code area}.
*
* @param lineStart the start of this line, inclusive .
* @param lineEnd the end of this line, exclusive.
* @param area the area inside which the text segments will be found.
* @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
* text segment.
* @param inclusionStrategy strategy for determining whether a text segment is inside the
* specified area.
* @return the start index of the first segment in the area.
*/
private int getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
@NonNull SegmentFinder segmentFinder,
@NonNull Layout.TextInclusionStrategy inclusionStrategy) {
if (lineStart >= lineEnd) return -1;
int runStart = lineStart;
int runLevel = -1;
// Check the BiDi runs and search for the start index.
for (int index = lineStart; index < lineEnd; ++index) {
final int level = getCharacterBidiLevel(index);
if (level != runLevel) {
final int start = getStartForRectWithinRun(runStart, index, area, segmentFinder,
inclusionStrategy);
if (start != -1) {
return start;
}
runStart = index;
runLevel = level;
}
}
return getStartForRectWithinRun(runStart, lineEnd, area, segmentFinder, inclusionStrategy);
}
/**
* Find the start character index of the first text segments within the directional run inside
* the specified {@code area}.
*
* @param runStart the start of this directional run, inclusive.
* @param runEnd the end of this directional run, exclusive.
* @param area the area inside which the text segments will be found.
* @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
* text segment.
* @param inclusionStrategy strategy for determining whether a text segment is inside the
* specified area.
* @return the start index of the first segment in the area.
*/
private int getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
@NonNull SegmentFinder segmentFinder,
@NonNull Layout.TextInclusionStrategy inclusionStrategy) {
if (runStart >= runEnd) return -1;
int segmentEndOffset = segmentFinder.nextEndBoundary(runStart);
// No segment is found in run.
if (segmentEndOffset == SegmentFinder.DONE) return -1;
int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
final RectF segmentBounds = new RectF();
while (segmentStartOffset != SegmentFinder.DONE && segmentStartOffset < runEnd) {
final int start = Math.max(runStart, segmentStartOffset);
final int end = Math.min(runEnd, segmentEndOffset);
getBoundsForRange(start, end, segmentBounds);
// Find the first segment inside the area, return the start.
if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return start;
segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset);
segmentEndOffset = segmentFinder.nextEndBoundary(segmentEndOffset);
}
return -1;
}
/**
* Find the end character index of the last text segments within a line inside the specified
* {@code area}.
*
* @param lineStart the start of this line, inclusive .
* @param lineEnd the end of this line, exclusive.
* @param area the area inside which the text segments will be found.
* @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
* text segment.
* @param inclusionStrategy strategy for determining whether a text segment is inside the
* specified area.
* @return the end index of the last segment in the area.
*/
private int getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
@NonNull SegmentFinder segmentFinder,
@NonNull Layout.TextInclusionStrategy inclusionStrategy) {
if (lineStart >= lineEnd) return -1;
lineStart = Math.max(lineStart, mStart);
lineEnd = Math.min(lineEnd, mEnd);
// The exclusive run end index.
int runEnd = lineEnd;
int runLevel = -1;
// Check the BiDi runs backwards and search for the end index.
for (int index = lineEnd - 1; index >= lineStart; --index) {
final int level = getCharacterBidiLevel(index);
if (level != runLevel) {
final int end = getEndForRectWithinRun(index + 1, runEnd, area, segmentFinder,
inclusionStrategy);
if (end != -1) return end;
runEnd = index + 1;
runLevel = level;
}
}
return getEndForRectWithinRun(lineStart, runEnd, area, segmentFinder, inclusionStrategy);
}
/**
* Find the end character index of the last text segments within the directional run inside the
* specified {@code area}.
*
* @param runStart the start of this directional run, inclusive.
* @param runEnd the end of this directional run, exclusive.
* @param area the area inside which the text segments will be found.
* @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
* text segment.
* @param inclusionStrategy strategy for determining whether a text segment is inside the
* specified area.
* @return the end index of the last segment in the area.
*/
private int getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
@NonNull SegmentFinder segmentFinder,
@NonNull Layout.TextInclusionStrategy inclusionStrategy) {
if (runStart >= runEnd) return -1;
int segmentStart = segmentFinder.previousStartBoundary(runEnd);
// No segment is found before the runEnd.
if (segmentStart == SegmentFinder.DONE) return -1;
int segmentEnd = segmentFinder.nextEndBoundary(segmentStart);
final RectF segmentBounds = new RectF();
while (segmentEnd != SegmentFinder.DONE && segmentEnd > runStart) {
final int start = Math.max(runStart, segmentStart);
final int end = Math.min(runEnd, segmentEnd);
getBoundsForRange(start, end, segmentBounds);
// Find the last segment inside the area, return the end.
if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return end;
segmentStart = segmentFinder.previousStartBoundary(segmentStart);
segmentEnd = segmentFinder.previousEndBoundary(segmentEnd);
}
return -1;
}
/**
* Get the vertical distance from the {@code pointF} to the {@code rectF}. It's useful to find
* the corresponding line for a given point.
*/
private static float verticalDistance(@NonNull RectF rectF, float y) {
if (rectF.top <= y && y < rectF.bottom) {
return 0f;
}
if (y < rectF.top) {
return rectF.top - y;
}
return y - rectF.bottom;
}
/**
* Describe the kinds of special objects contained in this Parcelable
* instance's marshaled representation. For example, if the object will
* include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
* the return value of this method must include the
* {@link #CONTENTS_FILE_DESCRIPTOR} bit.
*
* @return a bitmask indicating the set of special object types marshaled
* by this Parcelable object instance.
*/
@Override
public int describeContents() {
return 0;
}
/**
* Flatten this object in to a Parcel.
*
* @param dest The Parcel in which the object should be written.
* @param flags Additional flags about how the object should be written.
* May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mStart);
dest.writeInt(mEnd);
dest.writeFloatArray(mMatrixValues);
dest.writeFloatArray(mCharacterBounds);
// The end can also be a break position. We need an extra space to encode the breaks.
final int[] encodedFlags = Arrays.copyOf(mInternalCharacterFlags, mEnd - mStart + 1);
encodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, FLAG_GRAPHEME_SEGMENT_END,
mStart, mEnd, mGraphemeSegmentFinder);
encodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, FLAG_WORD_SEGMENT_END, mStart,
mEnd, mWordSegmentFinder);
encodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, FLAG_LINE_SEGMENT_END, mStart,
mEnd, mLineSegmentFinder);
dest.writeIntArray(encodedFlags);
}
private TextBoundsInfo(Parcel source) {
mStart = source.readInt();
mEnd = source.readInt();
mMatrixValues = Objects.requireNonNull(source.createFloatArray());
mCharacterBounds = Objects.requireNonNull(source.createFloatArray());
final int[] encodedFlags = Objects.requireNonNull(source.createIntArray());
mGraphemeSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START,
FLAG_GRAPHEME_SEGMENT_END, mStart, mEnd);
mWordSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START,
FLAG_WORD_SEGMENT_END, mStart, mEnd);
mLineSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START,
FLAG_LINE_SEGMENT_END, mStart, mEnd);
final int length = mEnd - mStart;
final int flagsMask = KNOWN_CHARACTER_FLAGS | BIDI_LEVEL_MASK;
mInternalCharacterFlags = new int[length];
for (int i = 0; i < length; ++i) {
// Remove the flags used to encoded segment boundaries.
mInternalCharacterFlags[i] = encodedFlags[i] & flagsMask;
}
}
private TextBoundsInfo(Builder builder) {
mStart = builder.mStart;
mEnd = builder.mEnd;
mMatrixValues = Arrays.copyOf(builder.mMatrixValues, 9);
final int length = mEnd - mStart;
mCharacterBounds = Arrays.copyOf(builder.mCharacterBounds, 4 * length);
// Store characterFlags and characterBidiLevels to save memory.
mInternalCharacterFlags = new int[length];
for (int index = 0; index < length; ++index) {
mInternalCharacterFlags[index] = builder.mCharacterFlags[index]
| (builder.mCharacterBidiLevels[index] << BIDI_LEVEL_SHIFT);
}
mGraphemeSegmentFinder = builder.mGraphemeSegmentFinder;
mWordSegmentFinder = builder.mWordSegmentFinder;
mLineSegmentFinder = builder.mLineSegmentFinder;
}
/**
* The CREATOR to make this class Parcelable.
*/
@NonNull
public static final Parcelable.Creator<TextBoundsInfo> CREATOR = new Creator<TextBoundsInfo>() {
@Override
public TextBoundsInfo createFromParcel(Parcel source) {
return new TextBoundsInfo(source);
}
@Override
public TextBoundsInfo[] newArray(int size) {
return new TextBoundsInfo[size];
}
};
private static final String TEXT_BOUNDS_INFO_KEY = "android.view.inputmethod.TextBoundsInfo";
/**
* Store the {@link TextBoundsInfo} into a {@link Bundle}. This method is used by
* {@link RemoteInputConnectionImpl} to transfer the {@link TextBoundsInfo} from the editor
* to IME.
*
* @see TextBoundsInfoResult
* @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)
* @hide
*/
@NonNull
public Bundle toBundle() {
final Bundle bundle = new Bundle();
bundle.putParcelable(TEXT_BOUNDS_INFO_KEY, this);
return bundle;
}
/** @hide */
@Nullable
public static TextBoundsInfo createFromBundle(@Nullable Bundle bundle) {
if (bundle == null) return null;
return bundle.getParcelable(TEXT_BOUNDS_INFO_KEY, TextBoundsInfo.class);
}
/**
* The builder class to create a {@link TextBoundsInfo} object.
*/
public static final class Builder {
private final float[] mMatrixValues = new float[9];
private boolean mMatrixInitialized;
private int mStart = -1;
private int mEnd = -1;
private float[] mCharacterBounds;
private int[] mCharacterFlags;
private int[] mCharacterBidiLevels;
private SegmentFinder mLineSegmentFinder;
private SegmentFinder mWordSegmentFinder;
private SegmentFinder mGraphemeSegmentFinder;
/**
* Create a builder for {@link TextBoundsInfo}.
* @param start the start index of the {@link TextBoundsInfo}, inclusive.
* @param end the end index of the {@link TextBoundsInfo}, exclusive.
* @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
* or {@code end} is smaller than the {@code start}.
*/
public Builder(int start, int end) {
setStartAndEnd(start, end);
}
/** Clear all the parameters set on this {@link Builder} to reuse it. */
@NonNull
public Builder clear() {
mMatrixInitialized = false;
mStart = -1;
mEnd = -1;
mCharacterBounds = null;
mCharacterFlags = null;
mCharacterBidiLevels = null;
mLineSegmentFinder = null;
mWordSegmentFinder = null;
mGraphemeSegmentFinder = null;
return this;
}
/**
* Sets the matrix that transforms local coordinates into screen coordinates.
*
* @param matrix transformation matrix from local coordinates into screen coordinates.
* @throws NullPointerException if the given {@code matrix} is {@code null}.
*/
@NonNull
public Builder setMatrix(@NonNull Matrix matrix) {
Objects.requireNonNull(matrix).getValues(mMatrixValues);
mMatrixInitialized = true;
return this;
}
/**
* Set the start and end index of the {@link TextBoundsInfo}. It's the range of the
* characters whose information is available in the {@link TextBoundsInfo}.
*
* @param start the start index of the {@link TextBoundsInfo}, inclusive.
* @param end the end index of the {@link TextBoundsInfo}, exclusive.
* @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
* or {@code end} is smaller than the {@code start}.
*/
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder")
public Builder setStartAndEnd(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
Preconditions.checkArgument(start >= 0);
Preconditions.checkArgumentInRange(start, 0, end, "start");
mStart = start;
mEnd = end;
return this;
}
/**
* Set the characters bounds, in the coordinates of the editor. <br/>
*
* The given array should be divided into groups of four where each element represents
* left, top, right and bottom of the character bounds respectively.
* The bounds of the i-th character in the editor should be stored at index
* 4 * (i - start). The length of the given array must equal to 4 * (end - start). <br/>
*
* Sometimes multiple characters in a single grapheme are rendered as one symbol on the
* screen. So those characters only have one shared bounds. In this case, we recommend the
* editor to assign all the width to the bounds of the first character in the grapheme,
* and make the rest characters' bounds zero-width. <br/>
*
* For example, the string "'0xD83D' '0xDE00'" is rendered as one grapheme - a grinning face
* emoji. If the bounds of the grapheme is: Rect(5, 10, 15, 20), the character bounds of the
* string should be: [ Rect(5, 10, 15, 20), Rect(15, 10, 15, 20) ].
*
* @param characterBounds the array of the flattened character bounds.
* @throws NullPointerException if the given {@code characterBounds} is {@code null}.
*/
@NonNull
public Builder setCharacterBounds(@NonNull float[] characterBounds) {
mCharacterBounds = Objects.requireNonNull(characterBounds);
return this;
}
/**
* Set the flags of the characters. The flags of the i-th character in the editor is stored
* at index (i - start). The length of the given array must equal to (end - start).
* The flags contain the following information:
* <ul>
* <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
* whitespace. </li>
* <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
* linefeed. </li>
* <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
* punctuation. </li>
* <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
* is RTL. All all character in the same line must have the same line direction. Check
* {@link #getLineSegmentFinder()} for more information of line boundaries. </li>
* </ul>
*
* @param characterFlags the array of the character's flags.
* @throws NullPointerException if the given {@code characterFlags} is {@code null}.
* @throws IllegalArgumentException if the given {@code characterFlags} contains invalid
* flags.
*
* @see #getCharacterFlags(int)
*/
@NonNull
public Builder setCharacterFlags(@NonNull int[] characterFlags) {
Objects.requireNonNull(characterFlags);
for (int characterFlag : characterFlags) {
if ((characterFlag & (~KNOWN_CHARACTER_FLAGS)) != 0) {
throw new IllegalArgumentException("characterFlags contains invalid flags.");
}
}
mCharacterFlags = characterFlags;
return this;
}
/**
* Set the BiDi levels for the character. The bidiLevel of the i-th character in the editor
* is stored at index (i - start). The length of the given array must equal to
* (end - start). <br/>
*
* BiDi level is defined by
* <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
* bidirectional algorithm </a>. One can determine whether a character's direction is
* right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
* If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
* character must be in the range of [0, 125].
* @param characterBidiLevels the array of the character's BiDi level.
*
* @throws NullPointerException if the given {@code characterBidiLevels} is {@code null}.
* @throws IllegalArgumentException if the given {@code characterBidiLevels} contains an
* element that's out of the range [0, 125].
*
* @see #getCharacterBidiLevel(int)
*/
@NonNull
public Builder setCharacterBidiLevel(@NonNull int[] characterBidiLevels) {
Objects.requireNonNull(characterBidiLevels);
for (int index = 0; index < characterBidiLevels.length; ++index) {
Preconditions.checkArgumentInRange(characterBidiLevels[index], 0, 125,
"bidiLevels[" + index + "]");
}
mCharacterBidiLevels = characterBidiLevels;
return this;
}
/**
* Set the {@link SegmentFinder} that locates the grapheme cluster boundaries. Grapheme is
* defined in <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">
* the unicode annex #29: unicode text segmentation<a/>. It's a user-perspective character.
* And it's usually the minimal unit for selection, backspace, deletion etc. <br/>
*
* Please note that only the grapheme segments within the range from start to end will
* be available to the IME. The remaining information will be discarded during serialization
* for better performance.
*
* @param graphemeSegmentFinder the {@link SegmentFinder} that locates the grapheme cluster
* boundaries.
* @throws NullPointerException if the given {@code graphemeSegmentFinder} is {@code null}.
*
* @see #getGraphemeSegmentFinder()
* @see SegmentFinder
* @see SegmentFinder.PrescribedSegmentFinder
*/
@NonNull
public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) {
mGraphemeSegmentFinder = Objects.requireNonNull(graphemeSegmentFinder);
return this;
}
/**
* Set the {@link SegmentFinder} that locates the word boundaries. <br/>
*
* Please note that only the word segments within the range from start to end will
* be available to the IME. The remaining information will be discarded during serialization
* for better performance.
* @param wordSegmentFinder set the {@link SegmentFinder} that locates the word boundaries.
* @throws NullPointerException if the given {@code wordSegmentFinder} is {@code null}.
*
* @see #getWordSegmentFinder()
* @see SegmentFinder
* @see SegmentFinder.PrescribedSegmentFinder
*/
@NonNull
public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) {
mWordSegmentFinder = Objects.requireNonNull(wordSegmentFinder);
return this;
}
/**
* Set the {@link SegmentFinder} that locates the line boundaries. Aside from the hard
* breaks in the text, it should also locate the soft line breaks added by the editor.
* It is expected that the characters within the same line is rendered on the same baseline.
* (Except for some text formatted as subscript and superscript.) <br/>
*
* Please note that only the line segments within the range from start to end will
* be available to the IME. The remaining information will be discarded during serialization
* for better performance.
* @param lineSegmentFinder set the {@link SegmentFinder} that locates the line boundaries.
* @throws NullPointerException if the given {@code lineSegmentFinder} is {@code null}.
*
* @see #getLineSegmentFinder()
* @see SegmentFinder
* @see SegmentFinder.PrescribedSegmentFinder
*/
@NonNull
public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) {
mLineSegmentFinder = Objects.requireNonNull(lineSegmentFinder);
return this;
}
/**
* Create the {@link TextBoundsInfo} using the parameters in this {@link Builder}.
*
* @throws IllegalStateException in the following conditions:
* <ul>
* <li>if the {@code start} or {@code end} is not set.</li>
* <li>if the {@code matrix} is not set.</li>
* <li>if {@code characterBounds} is not set or its length doesn't equal to
* 4 * ({@code end} - {@code start}).</li>
* <li>if the {@code characterFlags} is not set or its length doesn't equal to
* ({@code end} - {@code start}).</li>
* <li>if {@code graphemeSegmentFinder}, {@code wordSegmentFinder} or
* {@code lineSegmentFinder} is not set.</li>
* <li>if characters in the same line has inconsistent {@link #FLAG_LINE_IS_RTL}
* flag.</li>
* </ul>
*/
@NonNull
public TextBoundsInfo build() {
if (mStart < 0 || mEnd < 0) {
throw new IllegalStateException("Start and end must be set.");
}
if (!mMatrixInitialized) {
throw new IllegalStateException("Matrix must be set.");
}
if (mCharacterBounds == null) {
throw new IllegalStateException("CharacterBounds must be set.");
}
if (mCharacterFlags == null) {
throw new IllegalStateException("CharacterFlags must be set.");
}
if (mCharacterBidiLevels == null) {
throw new IllegalStateException("CharacterBidiLevel must be set.");
}
if (mCharacterBounds.length != 4 * (mEnd - mStart)) {
throw new IllegalStateException("The length of characterBounds doesn't match the "
+ "length of the given start and end."
+ " Expected length: " + (4 * (mEnd - mStart))
+ " characterBounds length: " + mCharacterBounds.length);
}
if (mCharacterFlags.length != mEnd - mStart) {
throw new IllegalStateException("The length of characterFlags doesn't match the "
+ "length of the given start and end."
+ " Expected length: " + (mEnd - mStart)
+ " characterFlags length: " + mCharacterFlags.length);
}
if (mCharacterBidiLevels.length != mEnd - mStart) {
throw new IllegalStateException("The length of characterBidiLevels doesn't match"
+ " the length of the given start and end."
+ " Expected length: " + (mEnd - mStart)
+ " characterFlags length: " + mCharacterBidiLevels.length);
}
if (mGraphemeSegmentFinder == null) {
throw new IllegalStateException("GraphemeSegmentFinder must be set.");
}
if (mWordSegmentFinder == null) {
throw new IllegalStateException("WordSegmentFinder must be set.");
}
if (mLineSegmentFinder == null) {
throw new IllegalStateException("LineSegmentFinder must be set.");
}
if (!isLineDirectionFlagConsistent(mCharacterFlags, mLineSegmentFinder, mStart, mEnd)) {
throw new IllegalStateException("characters in the same line must have the same "
+ "FLAG_LINE_IS_RTL flag value.");
}
return new TextBoundsInfo(this);
}
}
/**
* Encode the segment start and end positions in {@link SegmentFinder} to a flags array.
*
* For example:
* Text: "A BC DE"
* Input:
* start: 2, end: 7 // substring "BC DE"
* SegmentFinder: segment ranges = [(2, 4), (5, 7)] // a word break iterator
* flags: [0x0000, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000] // 0x0080 is whitespace
* segmentStartFlag: 0x0100
* segmentEndFlag: 0x0200
* Output:
* flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
* The index 2 and 5 encode segment starts, the index 4 and 7 encode a segment end.
*
* @param flags the flags array to receive the results.
* @param segmentStartFlag the flag used to encode the segment start.
* @param segmentEndFlag the flag used to encode the segment end.
* @param start the start index of the encoded range, inclusive.
* @param end the end index of the encoded range, inclusive.
* @param segmentFinder the SegmentFinder to be encoded.
*
* @see #decodeSegmentFinder(int[], int, int, int, int)
*/
private static void encodeSegmentFinder(@NonNull int[] flags, int segmentStartFlag,
int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder) {
if (end - start + 1 != flags.length) {
throw new IllegalStateException("The given flags array must have the same length as"
+ " the given range. flags length: " + flags.length
+ " range: [" + start + ", " + end + "]");
}
int segmentEnd = segmentFinder.nextEndBoundary(start);
if (segmentEnd == SegmentFinder.DONE) return;
int segmentStart = segmentFinder.previousStartBoundary(segmentEnd);
while (segmentEnd != SegmentFinder.DONE && segmentEnd <= end) {
if (segmentStart >= start) {
flags[segmentStart - start] |= segmentStartFlag;
flags[segmentEnd - start] |= segmentEndFlag;
}
segmentStart = segmentFinder.nextStartBoundary(segmentStart);
segmentEnd = segmentFinder.nextEndBoundary(segmentEnd);
}
}
/**
* Decode a {@link SegmentFinder} from a flags array.
*
* For example:
* Text: "A BC DE"
* Input:
* start: 2, end: 7 // substring "BC DE"
* flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
* segmentStartFlag: 0x0100
* segmentEndFlag: 0x0200
* Output:
* SegmentFinder: segment ranges = [(2, 4), (5, 7)]
*
* @param flags the flags array to decode the SegmentFinder.
* @param segmentStartFlag the flag to decode a segment start.
* @param segmentEndFlag the flag to decode a segment end.
* @param start the start index of the interested range, inclusive.
* @param end the end index of the interested range, inclusive.
*
* @see #encodeSegmentFinder(int[], int, int, int, int, SegmentFinder)
*/
private static SegmentFinder decodeSegmentFinder(int[] flags, int segmentStartFlag,
int segmentEndFlag, int start, int end) {
if (end - start + 1 != flags.length) {
throw new IllegalStateException("The given flags array must have the same length as"
+ " the given range. flags length: " + flags.length
+ " range: [" + start + ", " + end + "]");
}
int[] breaks = ArrayUtils.newUnpaddedIntArray(10);
int count = 0;
for (int offset = 0; offset < flags.length; ++offset) {
if ((flags[offset] & segmentStartFlag) == segmentStartFlag) {
breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
}
if ((flags[offset] & segmentEndFlag) == segmentEndFlag) {
breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
}
}
return new SegmentFinder.PrescribedSegmentFinder(Arrays.copyOf(breaks, count));
}
/**
* Check whether the {@link #FLAG_LINE_IS_RTL} is the same for characters in the same line.
* @return true if all characters in the same line has the same {@link #FLAG_LINE_IS_RTL} flag.
*/
private static boolean isLineDirectionFlagConsistent(int[] characterFlags,
SegmentFinder lineSegmentFinder, int start, int end) {
int segmentEnd = lineSegmentFinder.nextEndBoundary(start);
if (segmentEnd == SegmentFinder.DONE) return true;
int segmentStart = lineSegmentFinder.previousStartBoundary(segmentEnd);
while (segmentStart != SegmentFinder.DONE && segmentStart < end) {
final int lineStart = Math.max(segmentStart, start);
final int lineEnd = Math.min(segmentEnd, end);
final boolean lineIsRtl = (characterFlags[lineStart - start] & FLAG_LINE_IS_RTL) != 0;
for (int index = lineStart + 1; index < lineEnd; ++index) {
final int flags = characterFlags[index - start];
final boolean characterLineIsRtl = (flags & FLAG_LINE_IS_RTL) != 0;
if (characterLineIsRtl != lineIsRtl) {
return false;
}
}
segmentStart = lineSegmentFinder.nextStartBoundary(segmentStart);
segmentEnd = lineSegmentFinder.nextEndBoundary(segmentEnd);
}
return true;
}
}