214 lines
8.8 KiB
Java
214 lines
8.8 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.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];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|