/* * 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.text; import android.annotation.IntRange; import android.graphics.RectF; import androidx.annotation.NonNull; import com.android.internal.util.Preconditions; import java.util.Arrays; import java.util.Objects; /** * Finds text segment boundaries within text. Subclasses can implement different types of text * segments. Grapheme clusters and words are examples of possible text segments. These are * implemented by {@link GraphemeClusterSegmentFinder} and {@link WordSegmentFinder}. * *
Text segments may not overlap, so every character belongs to at most one text segment. A * character may belong to no text segments. * *
For example, WordSegmentFinder subdivides the text "Hello, World!" into four text segments: * "Hello", ",", "World", "!". The space character does not belong to any text segments. * * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy) */ public abstract class SegmentFinder { /** * Return value of previousStartBoundary(int), previousEndBoundary(int), nextStartBoundary(int), * and nextEndBoundary(int) when there are no boundaries of the specified type in the specified * direction. */ public static final int DONE = -1; /** * Returns the character offset of the previous text segment start boundary before the specified * character offset, or {@code DONE} if there are none. */ public abstract int previousStartBoundary(@IntRange(from = 0) int offset); /** * Returns the character offset of the previous text segment end boundary before the specified * character offset, or {@code DONE} if there are none. */ public abstract int previousEndBoundary(@IntRange(from = 0) int offset); /** * Returns the character offset of the next text segment start boundary after the specified * character offset, or {@code DONE} if there are none. */ public abstract int nextStartBoundary(@IntRange(from = 0) int offset); /** * Returns the character offset of the next text segment end boundary after the specified * character offset, or {@code DONE} if there are none. */ public abstract int nextEndBoundary(@IntRange(from = 0) int offset); /** * The default {@link SegmentFinder} implementation based on given segment ranges. */ public static class PrescribedSegmentFinder extends SegmentFinder { private final int[] mSegments; /** * Create a SegmentFinder with segments stored in an array, where i-th segment's start is * stored at segments[2 * i] and end is stored at segments[2 * i + 1] respectively. * *
It is required that segments do not overlap, and are already sorted by their start * indices.
* @param segments the array that stores the segment ranges. * @throws IllegalArgumentException if the given segments array's length is not even; the * given segments are not sorted or there are segments overlap with others. */ public PrescribedSegmentFinder(@NonNull int[] segments) { checkSegmentsValid(segments); mSegments = segments; } /** {@inheritDoc} */ @Override public int previousStartBoundary(@IntRange(from = 0) int offset) { return findPrevious(offset, /* isStart = */ true); } /** {@inheritDoc} */ @Override public int previousEndBoundary(@IntRange(from = 0) int offset) { return findPrevious(offset, /* isStart = */ false); } /** {@inheritDoc} */ @Override public int nextStartBoundary(@IntRange(from = 0) int offset) { return findNext(offset, /* isStart = */ true); } /** {@inheritDoc} */ @Override public int nextEndBoundary(@IntRange(from = 0) int offset) { return findNext(offset, /* isStart = */ false); } private int findNext(int offset, boolean isStart) { if (offset < 0) return DONE; if (mSegments.length < 1 || offset > mSegments[mSegments.length - 1]) return DONE; if (offset < mSegments[0]) { return isStart ? mSegments[0] : mSegments[1]; } int index = Arrays.binarySearch(mSegments, offset); if (index >= 0) { // mSegments may have duplicate elements (The previous segments end equals // to the following segments start.) Move the index forwards since we are searching // for the next segment. if (index + 1 < mSegments.length && mSegments[index + 1] == offset) { index = index + 1; } // Point the index to the first segment boundary larger than the given offset. index += 1; } else { // binarySearch returns the insertion point, it's the first segment boundary larger // than the given offset. index = -(index + 1); } if (index >= mSegments.length) return DONE; // +---------------------------------------+ // | | isStart | isEnd | // |---------------+-----------+-----------| // | indexIsStart | index | index + 1 | // |---------------+-----------+-----------| // | indexIsEnd | index + 1 | index | // +---------------------------------------+ boolean indexIsStart = index % 2 == 0; if (isStart != indexIsStart) { return (index + 1 < mSegments.length) ? mSegments[index + 1] : DONE; } return mSegments[index]; } private int findPrevious(int offset, boolean isStart) { if (mSegments.length < 1 || offset < mSegments[0]) return DONE; if (offset > mSegments[mSegments.length - 1]) { return isStart ? mSegments[mSegments.length - 2] : mSegments[mSegments.length - 1]; } int index = Arrays.binarySearch(mSegments, offset); if (index >= 0) { // mSegments may have duplicate elements (when the previous segments end equal // to the following segments start). Move the index backwards since we are searching // for the previous segment. if (index > 0 && mSegments[index - 1] == offset) { index = index - 1; } // Point the index to the first segment boundary smaller than the given offset. index -= 1; } else { // binarySearch returns the insertion point, insertionPoint - 1 is the first // segment boundary smaller than the given offset. index = -(index + 1) - 1; } if (index < 0) return DONE; // +---------------------------------------+ // | | isStart | isEnd | // |---------------+-----------+-----------| // | indexIsStart | index | index - 1 | // |---------------+-----------+-----------| // | indexIsEnd | index - 1 | index | // +---------------------------------------+ boolean indexIsStart = index % 2 == 0; if (isStart != indexIsStart) { return (index > 0) ? mSegments[index - 1] : DONE; } return mSegments[index]; } private static void checkSegmentsValid(int[] segments) { Objects.requireNonNull(segments); Preconditions.checkArgument(segments.length % 2 == 0, "the length of segments must be even"); if (segments.length == 0) return; int lastSegmentEnd = Integer.MIN_VALUE; for (int index = 0; index < segments.length; index += 2) { if (segments[index] < lastSegmentEnd) { throw new IllegalArgumentException("segments can't overlap"); } if (segments[index] >= segments[index + 1]) { throw new IllegalArgumentException("the segment range can't be empty"); } lastSegmentEnd = segments[index + 1]; } } } }