blob: 047d07a2e3e0143fc2d3d597aff63f89e1873390 [file] [log] [blame]
Aurimas Liutikasdc3f8852024-07-11 10:07:48 -07001/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
19import android.annotation.IntRange;
20import android.graphics.RectF;
21
22import androidx.annotation.NonNull;
23
24import com.android.internal.util.Preconditions;
25
26import java.util.Arrays;
27import java.util.Objects;
28
29/**
30 * Finds text segment boundaries within text. Subclasses can implement different types of text
31 * segments. Grapheme clusters and words are examples of possible text segments. These are
32 * implemented by {@link GraphemeClusterSegmentFinder} and {@link WordSegmentFinder}.
33 *
34 * <p>Text segments may not overlap, so every character belongs to at most one text segment. A
35 * character may belong to no text segments.
36 *
37 * <p>For example, WordSegmentFinder subdivides the text "Hello, World!" into four text segments:
38 * "Hello", ",", "World", "!". The space character does not belong to any text segments.
39 *
40 * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)
41 */
42public abstract class SegmentFinder {
43 /**
44 * Return value of previousStartBoundary(int), previousEndBoundary(int), nextStartBoundary(int),
45 * and nextEndBoundary(int) when there are no boundaries of the specified type in the specified
46 * direction.
47 */
48 public static final int DONE = -1;
49
50 /**
51 * Returns the character offset of the previous text segment start boundary before the specified
52 * character offset, or {@code DONE} if there are none.
53 */
54 public abstract int previousStartBoundary(@IntRange(from = 0) int offset);
55
56 /**
57 * Returns the character offset of the previous text segment end boundary before the specified
58 * character offset, or {@code DONE} if there are none.
59 */
60 public abstract int previousEndBoundary(@IntRange(from = 0) int offset);
61
62 /**
63 * Returns the character offset of the next text segment start boundary after the specified
64 * character offset, or {@code DONE} if there are none.
65 */
66 public abstract int nextStartBoundary(@IntRange(from = 0) int offset);
67
68 /**
69 * Returns the character offset of the next text segment end boundary after the specified
70 * character offset, or {@code DONE} if there are none.
71 */
72 public abstract int nextEndBoundary(@IntRange(from = 0) int offset);
73
74 /**
75 * The default {@link SegmentFinder} implementation based on given segment ranges.
76 */
77 public static class PrescribedSegmentFinder extends SegmentFinder {
78 private final int[] mSegments;
79
80 /**
81 * Create a SegmentFinder with segments stored in an array, where i-th segment's start is
82 * stored at segments[2 * i] and end is stored at segments[2 * i + 1] respectively.
83 *
84 * <p> It is required that segments do not overlap, and are already sorted by their start
85 * indices. </p>
86 * @param segments the array that stores the segment ranges.
87 * @throws IllegalArgumentException if the given segments array's length is not even; the
88 * given segments are not sorted or there are segments overlap with others.
89 */
90 public PrescribedSegmentFinder(@NonNull int[] segments) {
91 checkSegmentsValid(segments);
92 mSegments = segments;
93 }
94
95 /** {@inheritDoc} */
96 @Override
97 public int previousStartBoundary(@IntRange(from = 0) int offset) {
98 return findPrevious(offset, /* isStart = */ true);
99 }
100
101 /** {@inheritDoc} */
102 @Override
103 public int previousEndBoundary(@IntRange(from = 0) int offset) {
104 return findPrevious(offset, /* isStart = */ false);
105 }
106
107 /** {@inheritDoc} */
108 @Override
109 public int nextStartBoundary(@IntRange(from = 0) int offset) {
110 return findNext(offset, /* isStart = */ true);
111 }
112
113 /** {@inheritDoc} */
114 @Override
115 public int nextEndBoundary(@IntRange(from = 0) int offset) {
116 return findNext(offset, /* isStart = */ false);
117 }
118
119 private int findNext(int offset, boolean isStart) {
120 if (offset < 0) return DONE;
121 if (mSegments.length < 1 || offset > mSegments[mSegments.length - 1]) return DONE;
122
123 if (offset < mSegments[0]) {
124 return isStart ? mSegments[0] : mSegments[1];
125 }
126
127 int index = Arrays.binarySearch(mSegments, offset);
128 if (index >= 0) {
129 // mSegments may have duplicate elements (The previous segments end equals
130 // to the following segments start.) Move the index forwards since we are searching
131 // for the next segment.
132 if (index + 1 < mSegments.length && mSegments[index + 1] == offset) {
133 index = index + 1;
134 }
135 // Point the index to the first segment boundary larger than the given offset.
136 index += 1;
137 } else {
138 // binarySearch returns the insertion point, it's the first segment boundary larger
139 // than the given offset.
140 index = -(index + 1);
141 }
142 if (index >= mSegments.length) return DONE;
143
144 // +---------------------------------------+
145 // | | isStart | isEnd |
146 // |---------------+-----------+-----------|
147 // | indexIsStart | index | index + 1 |
148 // |---------------+-----------+-----------|
149 // | indexIsEnd | index + 1 | index |
150 // +---------------------------------------+
151 boolean indexIsStart = index % 2 == 0;
152 if (isStart != indexIsStart) {
153 return (index + 1 < mSegments.length) ? mSegments[index + 1] : DONE;
154 }
155 return mSegments[index];
156 }
157
158 private int findPrevious(int offset, boolean isStart) {
159 if (mSegments.length < 1 || offset < mSegments[0]) return DONE;
160
161 if (offset > mSegments[mSegments.length - 1]) {
162 return isStart ? mSegments[mSegments.length - 2] : mSegments[mSegments.length - 1];
163 }
164
165 int index = Arrays.binarySearch(mSegments, offset);
166 if (index >= 0) {
167 // mSegments may have duplicate elements (when the previous segments end equal
168 // to the following segments start). Move the index backwards since we are searching
169 // for the previous segment.
170 if (index > 0 && mSegments[index - 1] == offset) {
171 index = index - 1;
172 }
173 // Point the index to the first segment boundary smaller than the given offset.
174 index -= 1;
175 } else {
176 // binarySearch returns the insertion point, insertionPoint - 1 is the first
177 // segment boundary smaller than the given offset.
178 index = -(index + 1) - 1;
179 }
180 if (index < 0) return DONE;
181
182 // +---------------------------------------+
183 // | | isStart | isEnd |
184 // |---------------+-----------+-----------|
185 // | indexIsStart | index | index - 1 |
186 // |---------------+-----------+-----------|
187 // | indexIsEnd | index - 1 | index |
188 // +---------------------------------------+
189 boolean indexIsStart = index % 2 == 0;
190 if (isStart != indexIsStart) {
191 return (index > 0) ? mSegments[index - 1] : DONE;
192 }
193 return mSegments[index];
194 }
195
196 private static void checkSegmentsValid(int[] segments) {
197 Objects.requireNonNull(segments);
198 Preconditions.checkArgument(segments.length % 2 == 0,
199 "the length of segments must be even");
200 if (segments.length == 0) return;
201 int lastSegmentEnd = Integer.MIN_VALUE;
202 for (int index = 0; index < segments.length; index += 2) {
203 if (segments[index] < lastSegmentEnd) {
204 throw new IllegalArgumentException("segments can't overlap");
205 }
206 if (segments[index] >= segments[index + 1]) {
207 throw new IllegalArgumentException("the segment range can't be empty");
208 }
209 lastSegmentEnd = segments[index + 1];
210 }
211 }
212 }
213}