script-astra/Android/Sdk/sources/android-35/android/text/SegmentFinder.java

214 lines
8.8 KiB
Java
Raw Normal View History

2025-01-20 15:15:20 +00:00
/*
* 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}.
*
* <p>Text segments may not overlap, so every character belongs to at most one text segment. A
* character may belong to no text segments.
*
* <p>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.
*
* <p> It is required that segments do not overlap, and are already sorted by their start
* indices. </p>
* @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];
}
}
}
}