Aurimas Liutikas | dc3f885 | 2024-07-11 10:07:48 -0700 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package android.text; |
| 18 | |
| 19 | import android.annotation.IntRange; |
| 20 | import android.graphics.RectF; |
| 21 | |
| 22 | import androidx.annotation.NonNull; |
| 23 | |
| 24 | import com.android.internal.util.Preconditions; |
| 25 | |
| 26 | import java.util.Arrays; |
| 27 | import 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 | */ |
| 42 | public 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 | } |