/* * 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. * *
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. *
*/ 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: *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 * unicode bidirectional * algorithm. *
* *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). *
* *Under the following conditions, this method will return -1 indicating that no valid * character is found: *
* For example: * (L represents LTR character, and R represents RTL character. The number is the index) *
* 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 ** * 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. *
* render result: L0 L1 L2 R5 R4 R3 L6 L7 L8 * position after L2: | * position before R3: | * result position: | ** * 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. *
* render result: L0 L1 L2 R5 R4 R3 L6 L7 L8 * position after R5: | * position before L6: | * result position: | ** * This method helps guarantee that the cursor index and the cursor position forms a one to * one relation. * * * @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)}. * *
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 * unicode bidirectional * algorithm. *
* *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). *
* * @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