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.view.inputmethod; 18 19 import android.annotation.IntDef; 20 import android.annotation.IntRange; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.graphics.Matrix; 24 import android.graphics.RectF; 25 import android.os.Bundle; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.text.Layout; 29 import android.text.SegmentFinder; 30 31 import com.android.internal.util.ArrayUtils; 32 import com.android.internal.util.GrowingArrayUtils; 33 import com.android.internal.util.Preconditions; 34 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 import java.util.Arrays; 38 import java.util.Objects; 39 import java.util.concurrent.Executor; 40 import java.util.function.Consumer; 41 42 /** 43 * The text bounds information of a slice of text in the editor. 44 * 45 * <p> This class provides IME the layout information of the text within the range from 46 * {@link #getStartIndex()} to {@link #getEndIndex()}. It's intended to be used by IME as a 47 * supplementary API to support handwriting gestures. 48 * </p> 49 */ 50 public final class TextBoundsInfo implements Parcelable { 51 /** 52 * The flag indicating that the character is a whitespace. 53 * 54 * @see Builder#setCharacterFlags(int[]) 55 * @see #getCharacterFlags(int) 56 */ 57 public static final int FLAG_CHARACTER_WHITESPACE = 1; 58 59 /** 60 * The flag indicating that the character is a linefeed character. 61 * 62 * @see Builder#setCharacterFlags(int[]) 63 * @see #getCharacterFlags(int) 64 */ 65 public static final int FLAG_CHARACTER_LINEFEED = 1 << 1; 66 67 /** 68 * The flag indicating that the character is a punctuation. 69 * 70 * @see Builder#setCharacterFlags(int[]) 71 * @see #getCharacterFlags(int) 72 */ 73 public static final int FLAG_CHARACTER_PUNCTUATION = 1 << 2; 74 75 /** 76 * The flag indicating that the line this character belongs to has RTL line direction. It's 77 * required that all characters in the same line must have the same direction. 78 * 79 * @see Builder#setCharacterFlags(int[]) 80 * @see #getCharacterFlags(int) 81 */ 82 public static final int FLAG_LINE_IS_RTL = 1 << 3; 83 84 85 /** @hide */ 86 @IntDef(prefix = "FLAG_", flag = true, value = { 87 FLAG_CHARACTER_WHITESPACE, 88 FLAG_CHARACTER_LINEFEED, 89 FLAG_CHARACTER_PUNCTUATION, 90 FLAG_LINE_IS_RTL 91 }) 92 @Retention(RetentionPolicy.SOURCE) 93 public @interface CharacterFlags {} 94 95 /** All the valid flags. */ 96 private static final int KNOWN_CHARACTER_FLAGS = FLAG_CHARACTER_WHITESPACE 97 | FLAG_CHARACTER_LINEFEED | FLAG_CHARACTER_PUNCTUATION | FLAG_LINE_IS_RTL; 98 99 /** 100 * The amount of shift to get the character's BiDi level from the internal character flags. 101 */ 102 private static final int BIDI_LEVEL_SHIFT = 19; 103 104 /** 105 * The mask used to get the character's BiDi level from the internal character flags. 106 */ 107 private static final int BIDI_LEVEL_MASK = 0x7F << BIDI_LEVEL_SHIFT; 108 109 /** 110 * The flag indicating that the character at the index is the start of a line segment. 111 * This flag is only used internally to serialize the {@link SegmentFinder}. 112 * 113 * @see #writeToParcel(Parcel, int) 114 */ 115 private static final int FLAG_LINE_SEGMENT_START = 1 << 31; 116 117 /** 118 * The flag indicating that the character at the index is the end of a line segment. 119 * This flag is only used internally to serialize the {@link SegmentFinder}. 120 * 121 * @see #writeToParcel(Parcel, int) 122 */ 123 private static final int FLAG_LINE_SEGMENT_END = 1 << 30; 124 125 /** 126 * The flag indicating that the character at the index is the start of a word segment. 127 * This flag is only used internally to serialize the {@link SegmentFinder}. 128 * 129 * @see #writeToParcel(Parcel, int) 130 */ 131 private static final int FLAG_WORD_SEGMENT_START = 1 << 29; 132 133 /** 134 * The flag indicating that the character at the index is the end of a word segment. 135 * This flag is only used internally to serialize the {@link SegmentFinder}. 136 * 137 * @see #writeToParcel(Parcel, int) 138 */ 139 private static final int FLAG_WORD_SEGMENT_END = 1 << 28; 140 141 /** 142 * The flag indicating that the character at the index is the start of a grapheme segment. 143 * It's only used internally to serialize the {@link SegmentFinder}. 144 * 145 * @see #writeToParcel(Parcel, int) 146 */ 147 private static final int FLAG_GRAPHEME_SEGMENT_START = 1 << 27; 148 149 /** 150 * The flag indicating that the character at the index is the end of a grapheme segment. 151 * It's only used internally to serialize the {@link SegmentFinder}. 152 * 153 * @see #writeToParcel(Parcel, int) 154 */ 155 private static final int FLAG_GRAPHEME_SEGMENT_END = 1 << 26; 156 157 private final int mStart; 158 private final int mEnd; 159 private final float[] mMatrixValues; 160 private final float[] mCharacterBounds; 161 /** 162 * The array that encodes character and BiDi levels. They are stored together to save memory 163 * space, and it's easier during serialization. 164 */ 165 private final int[] mInternalCharacterFlags; 166 private final SegmentFinder mLineSegmentFinder; 167 private final SegmentFinder mWordSegmentFinder; 168 private final SegmentFinder mGraphemeSegmentFinder; 169 170 /** 171 * Set the given {@link android.graphics.Matrix} to be the transformation 172 * matrix that is to be applied other positional data in this class. 173 */ 174 @NonNull getMatrix(@onNull Matrix matrix)175 public void getMatrix(@NonNull Matrix matrix) { 176 Objects.requireNonNull(matrix); 177 matrix.setValues(mMatrixValues); 178 } 179 180 /** 181 * Returns the index of the first character whose bounds information is available in this 182 * {@link TextBoundsInfo}, inclusive. 183 * 184 * @see Builder#setStartAndEnd(int, int) 185 */ getStartIndex()186 public int getStartIndex() { 187 return mStart; 188 } 189 190 /** 191 * Returns the index of the last character whose bounds information is available in this 192 * {@link TextBoundsInfo}, exclusive. 193 * 194 * @see Builder#setStartAndEnd(int, int) 195 */ getEndIndex()196 public int getEndIndex() { 197 return mEnd; 198 } 199 200 /** 201 * Set the bounds of the character at the given {@code index} to the given {@link RectF}, in 202 * the coordinates of the editor. 203 * 204 * @param index the index of the queried character. 205 * @param bounds the {@link RectF} used to receive the result. 206 * 207 * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from 208 * the {@code start} to the {@code end}. 209 */ 210 @NonNull getCharacterBounds(int index, @NonNull RectF bounds)211 public void getCharacterBounds(int index, @NonNull RectF bounds) { 212 if (index < mStart || index >= mEnd) { 213 throw new IndexOutOfBoundsException("Index is out of the bounds of " 214 + "[" + mStart + ", " + mEnd + ")."); 215 } 216 final int offset = 4 * (index - mStart); 217 bounds.set(mCharacterBounds[offset], mCharacterBounds[offset + 1], 218 mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]); 219 } 220 221 /** 222 * Return the flags associated with the character at the given {@code index}. 223 * The flags contain the following information: 224 * <ul> 225 * <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a 226 * whitespace. </li> 227 * <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a 228 * linefeed. </li> 229 * <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a 230 * punctuation. </li> 231 * <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to 232 * has RTL line direction. All characters in the same line must have the same line 233 * direction. Check {@link #getLineSegmentFinder()} for more information of 234 * line boundaries. </li> 235 * </ul> 236 * 237 * @param index the index of the queried character. 238 * @return the flags associated with the queried character. 239 * 240 * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from 241 * the {@code start} to the {@code end}. 242 * 243 * @see #FLAG_CHARACTER_WHITESPACE 244 * @see #FLAG_CHARACTER_LINEFEED 245 * @see #FLAG_CHARACTER_PUNCTUATION 246 * @see #FLAG_LINE_IS_RTL 247 */ 248 @CharacterFlags getCharacterFlags(int index)249 public int getCharacterFlags(int index) { 250 if (index < mStart || index >= mEnd) { 251 throw new IndexOutOfBoundsException("Index is out of the bounds of " 252 + "[" + mStart + ", " + mEnd + ")."); 253 } 254 final int offset = index - mStart; 255 return mInternalCharacterFlags[offset] & KNOWN_CHARACTER_FLAGS; 256 } 257 258 /** 259 * The BiDi level of the character at the given {@code index}. <br/> 260 * BiDi level is defined by 261 * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode 262 * bidirectional algorithm </a>. One can determine whether a character's direction is 263 * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level. 264 * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a 265 * character must be in the range of [0, 125]. 266 * 267 * @param index the index of the queried character. 268 * @return the BiDi level of the character, which is an integer in the range of [0, 125]. 269 * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from 270 * the {@code start} to the {@code end}. 271 * 272 * @see Builder#setCharacterBidiLevel(int[]) 273 */ 274 @IntRange(from = 0, to = 125) getCharacterBidiLevel(int index)275 public int getCharacterBidiLevel(int index) { 276 if (index < mStart || index >= mEnd) { 277 throw new IndexOutOfBoundsException("Index is out of the bounds of " 278 + "[" + mStart + ", " + mEnd + ")."); 279 } 280 final int offset = index - mStart; 281 return (mInternalCharacterFlags[offset] & BIDI_LEVEL_MASK) >> BIDI_LEVEL_SHIFT; 282 } 283 284 /** 285 * Returns the {@link SegmentFinder} that locates the word boundaries. 286 * 287 * @see Builder#setWordSegmentFinder(SegmentFinder) 288 */ 289 @NonNull getWordSegmentFinder()290 public SegmentFinder getWordSegmentFinder() { 291 return mWordSegmentFinder; 292 } 293 294 /** 295 * Returns the {@link SegmentFinder} that locates the grapheme boundaries. 296 * 297 * @see Builder#setGraphemeSegmentFinder(SegmentFinder) 298 */ 299 @NonNull getGraphemeSegmentFinder()300 public SegmentFinder getGraphemeSegmentFinder() { 301 return mGraphemeSegmentFinder; 302 } 303 304 /** 305 * Returns the {@link SegmentFinder} that locates the line boundaries. 306 * 307 * @see Builder#setLineSegmentFinder(SegmentFinder) 308 */ 309 @NonNull getLineSegmentFinder()310 public SegmentFinder getLineSegmentFinder() { 311 return mLineSegmentFinder; 312 } 313 314 /** 315 * Return the index of the closest character to the given position. 316 * It's similar to the text layout API {@link Layout#getOffsetForHorizontal(int, float)}. 317 * And it's mainly used to find the cursor index (the index of the character before which the 318 * cursor should be placed) for the given position. It's guaranteed that the returned index is 319 * a grapheme break. Check {@link #getGraphemeSegmentFinder()} for more information. 320 * 321 * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom and each 322 * line is laid out according to the display algorithm specified in 323 * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional 324 * algorithm</a>. 325 * </p> 326 * 327 * <p> This method won't check the text ranges whose line information is missing. For example, 328 * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated 329 * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method 330 * won't check the text in the ranges of [5, 7) and [12, 15). 331 * </p> 332 * 333 * <p> Under the following conditions, this method will return -1 indicating that no valid 334 * character is found: 335 * <ul> 336 * <li> The given {@code y} coordinate is above the first line or below the last line (the 337 * first line or the last line is identified by the {@link SegmentFinder} returned from 338 * {@link #getLineSegmentFinder()}). </li> 339 * <li> There is no character in this {@link TextBoundsInfo}. </li> 340 * </ul> 341 * </p> 342 * 343 * @param x the x coordinates of the interested location, in the editor's coordinates. 344 * @param y the y coordinates of the interested location, in the editor's coordinates. 345 * @return the index of the character whose position is closest to the given location. It will 346 * return -1 if it can't find a character. 347 * 348 * @see Layout#getOffsetForHorizontal(int, float) 349 */ getOffsetForPosition(float x, float y)350 public int getOffsetForPosition(float x, float y) { 351 final int[] lineRange = new int[2]; 352 final RectF lineBounds = new RectF(); 353 getLineInfo(y, lineRange, lineBounds); 354 // No line is found, return -1; 355 if (lineRange[0] == -1 || lineRange[1] == -1) return -1; 356 final int lineStart = lineRange[0]; 357 final int lineEnd = lineRange[1]; 358 359 final boolean lineEndsWithLinefeed = 360 (getCharacterFlags(lineEnd - 1) & FLAG_CHARACTER_LINEFEED) != 0; 361 362 // Consider the following 2 cases: 363 // Case 1: 364 // Text: "AB\nCD" 365 // Layout: AB 366 // CD 367 // Case 2: 368 // Text: "ABCD" 369 // Layout: AB 370 // CD 371 // If user wants to insert a 'X' character at the end of the first line: 372 // In case 1, 'X' is inserted before the last character '\n'. 373 // In case 2, 'X' is inserted after the last character 'B'. 374 // So if a line ends with linefeed, it shouldn't check the cursor position after the last 375 // character. 376 final int lineLimit; 377 if (lineEndsWithLinefeed) { 378 lineLimit = lineEnd; 379 } else { 380 lineLimit = lineEnd + 1; 381 } 382 // Point graphemeStart to the start of the first grapheme segment intersects with the line. 383 int graphemeStart = mGraphemeSegmentFinder.nextEndBoundary(lineStart); 384 // The grapheme information is missing. 385 if (graphemeStart == SegmentFinder.DONE) return -1; 386 graphemeStart = mGraphemeSegmentFinder.previousStartBoundary(graphemeStart); 387 388 int target = -1; 389 float minDistance = Float.MAX_VALUE; 390 while (graphemeStart != SegmentFinder.DONE && graphemeStart < lineLimit) { 391 if (graphemeStart >= lineStart) { 392 float cursorPosition = getCursorHorizontalPosition(graphemeStart, lineStart, 393 lineEnd, lineBounds.left, lineBounds.right); 394 final float distance = Math.abs(cursorPosition - x); 395 if (distance < minDistance) { 396 minDistance = distance; 397 target = graphemeStart; 398 } 399 } 400 graphemeStart = mGraphemeSegmentFinder.nextStartBoundary(graphemeStart); 401 } 402 403 return target; 404 } 405 406 /** 407 * Whether the primary position at the given index is the previous character's trailing 408 * position. <br/> 409 * 410 * For LTR character, trailing position is its right edge. For RTL character, trailing position 411 * is its left edge. 412 * 413 * The primary position is defined as the position of a newly inserted character with the 414 * context direction at the given offset. In contrast, the secondary position is the position 415 * of a newly inserted character with the context's opposite direction at the given offset. 416 * 417 * In Android, the trailing position is used for primary position when the direction run after 418 * the given index has a higher level than the current direction run. 419 * 420 * <p> 421 * For example: 422 * (L represents LTR character, and R represents RTL character. The number is the index) 423 * <pre> 424 * input text: L0 L1 L2 R3 R4 R5 L6 L7 L8 425 * render result: L0 L1 L2 R5 R4 R3 L6 L7 L8 426 * BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ] 427 * BiDi Level: 0 0 0 1 1 1 0 0 0 428 * </pre> 429 * 430 * The index 3 is a BiDi transition point, the cursor can be placed either after L2 or before 431 * R3. Because the bidi level of run 1 is higher than the run 0, this method returns true. And 432 * the cursor should be placed after L2. 433 * <pre> 434 * render result: L0 L1 L2 R5 R4 R3 L6 L7 L8 435 * position after L2: | 436 * position before R3: | 437 * result position: | 438 * </pre> 439 * 440 * The index 6 is also a Bidi transition point, the 2 possible cursor positions are exactly the 441 * same as index 3. However, since the bidi level of run 2 is higher than the run 1, this 442 * method returns false. And the cursor should be placed before L6. 443 * <pre> 444 * render result: L0 L1 L2 R5 R4 R3 L6 L7 L8 445 * position after R5: | 446 * position before L6: | 447 * result position: | 448 * </pre> 449 * 450 * This method helps guarantee that the cursor index and the cursor position forms a one to 451 * one relation. 452 * </p> 453 * 454 * @param offset the offset of the character in front of which the cursor is placed. It must be 455 * the start index of a grapheme. And it must be in the range from lineStart to 456 * lineEnd. An offset equal to lineEnd is allowed. It indicates that the cursor is 457 * placed at the end of current line instead of the start of the following line. 458 * @param lineStart the start index of the line that index belongs to, inclusive. 459 * @param lineEnd the end index of the line that index belongs to, exclusive. 460 * @return true if primary position is the trailing position of the previous character. 461 * 462 * @see #getCursorHorizontalPosition(int, int, int, float, float) 463 */ primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd)464 private boolean primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd) { 465 final int bidiLevel; 466 if (offset < lineEnd) { 467 bidiLevel = getCharacterBidiLevel(offset); 468 } else { 469 // index equals to lineEnd, use line's BiDi level for the BiDi run. 470 boolean lineIsRtl = 471 (getCharacterFlags(offset - 1) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL; 472 bidiLevel = lineIsRtl ? 1 : 0; 473 } 474 final int bidiLevelBefore; 475 if (offset > lineStart) { 476 // Here it assumes index is always the start of a grapheme. And (index - 1) belongs to 477 // the previous grapheme. 478 bidiLevelBefore = getCharacterBidiLevel(offset - 1); 479 } else { 480 // index equals to lineStart, use line's BiDi level for previous BiDi run. 481 boolean lineIsRtl = 482 (getCharacterFlags(offset) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL; 483 bidiLevelBefore = lineIsRtl ? 1 : 0; 484 } 485 return bidiLevelBefore < bidiLevel; 486 } 487 488 /** 489 * Returns the x coordinates of the cursor at the given index. (The index of the character 490 * before which the cursor should be placed.) 491 * 492 * @param index the character index before which the cursor is placed. It must be the start 493 * index of a grapheme. It must be in the range from lineStart to lineEnd. 494 * An index equal to lineEnd is allowed. It indicates that the cursor is 495 * placed at the end of current line instead of the start of the following line. 496 * @param lineStart start index of the line that index belongs to, inclusive. 497 * @param lineEnd end index of the line that index belongs, exclusive. 498 * @return the x coordinates of the cursor at the given index, 499 * 500 * @see #primaryIsTrailingPrevious(int, int, int) 501 */ getCursorHorizontalPosition(int index, int lineStart, int lineEnd, float lineLeft, float lineRight)502 private float getCursorHorizontalPosition(int index, int lineStart, int lineEnd, 503 float lineLeft, float lineRight) { 504 Preconditions.checkArgumentInRange(index, lineStart, lineEnd, "index"); 505 final boolean lineIsRtl = (getCharacterFlags(lineStart) & FLAG_LINE_IS_RTL) != 0; 506 final boolean isPrimaryIsTrailingPrevious = 507 primaryIsTrailingPrevious(index, lineStart, lineEnd); 508 509 // The index of the character used to compute the cursor position. 510 final int targetIndex; 511 // Whether to use the start position of the character. 512 // For LTR character start is the left edge. For RTL character, start is the right edge. 513 final boolean isStart; 514 if (isPrimaryIsTrailingPrevious) { 515 // (index - 1) belongs to the previous line(if any), return the line start position. 516 if (index <= lineStart) { 517 return lineIsRtl ? lineRight : lineLeft; 518 } 519 targetIndex = index - 1; 520 isStart = false; 521 } else { 522 // index belongs to the next line(if any), return the line end position. 523 if (index >= lineEnd) { 524 return lineIsRtl ? lineLeft : lineRight; 525 } 526 targetIndex = index; 527 isStart = true; 528 } 529 530 // The BiDi level is odd when the character is RTL. 531 final boolean isRtl = (getCharacterBidiLevel(targetIndex) & 1) != 0; 532 final int offset = targetIndex - mStart; 533 // If the character is RTL, the start is the right edge. Otherwise, the start is the 534 // left edge: 535 // +-----------------------+ 536 // | | start | end | 537 // |-------+-------+-------| 538 // | RTL | right | left | 539 // |-------+-------+-------| 540 // | LTR | left | right | 541 // +-------+-------+-------+ 542 return (isRtl != isStart) ? mCharacterBounds[4 * offset] : mCharacterBounds[4 * offset + 2]; 543 } 544 545 /** 546 * Return the minimal rectangle that contains all the characters in the given range. 547 * 548 * @param start the start index of the given range, inclusive. 549 * @param end the end index of the given range, exclusive. 550 * @param rectF the {@link RectF} to receive the bounds. 551 */ getBoundsForRange(int start, int end, @NonNull RectF rectF)552 private void getBoundsForRange(int start, int end, @NonNull RectF rectF) { 553 Preconditions.checkArgumentInRange(start, mStart, mEnd - 1, "start"); 554 Preconditions.checkArgumentInRange(end, start, mEnd, "end"); 555 if (end <= start) { 556 rectF.setEmpty(); 557 return; 558 } 559 560 rectF.left = Float.MAX_VALUE; 561 rectF.top = Float.MAX_VALUE; 562 rectF.right = Float.MIN_VALUE; 563 rectF.bottom = Float.MIN_VALUE; 564 for (int index = start; index < end; ++index) { 565 final int offset = index - mStart; 566 rectF.left = Math.min(rectF.left, mCharacterBounds[4 * offset]); 567 rectF.top = Math.min(rectF.top, mCharacterBounds[4 * offset + 1]); 568 rectF.right = Math.max(rectF.right, mCharacterBounds[4 * offset + 2]); 569 rectF.bottom = Math.max(rectF.bottom, mCharacterBounds[4 * offset + 3]); 570 } 571 } 572 573 /** 574 * Return the character range and bounds of the closest line to the given {@code y} coordinate, 575 * in the editor's local coordinates. 576 * 577 * If the given y is above the first line or below the last line -1 will be returned for line 578 * start and end. 579 * 580 * This method assumes that the lines are laid out from the top to bottom. 581 * 582 * @param y the y coordinates used to search for the line. 583 * @param characterRange a two element array used to receive the character range of the line. 584 * If no valid line is found -1 will be returned for both start and end. 585 * @param bounds {@link RectF} to receive the line bounds result, nullable. If given, it can 586 * still be modified even if no valid line is found. 587 */ getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds)588 private void getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds) { 589 characterRange[0] = -1; 590 characterRange[1] = -1; 591 592 // Starting from the first line. 593 int currentLineEnd = mLineSegmentFinder.nextEndBoundary(mStart); 594 if (currentLineEnd == SegmentFinder.DONE) return; 595 int currentLineStart = mLineSegmentFinder.previousStartBoundary(currentLineEnd); 596 597 float top = Float.MAX_VALUE; 598 float bottom = Float.MIN_VALUE; 599 float minDistance = Float.MAX_VALUE; 600 final RectF currentLineBounds = new RectF(); 601 while (currentLineStart != SegmentFinder.DONE && currentLineStart < mEnd) { 602 final int lineStartInRange = Math.max(mStart, currentLineStart); 603 final int lineEndInRange = Math.min(mEnd, currentLineEnd); 604 getBoundsForRange(lineStartInRange, lineEndInRange, currentLineBounds); 605 606 top = Math.min(currentLineBounds.top, top); 607 bottom = Math.max(currentLineBounds.bottom, bottom); 608 609 final float distance = verticalDistance(currentLineBounds, y); 610 611 if (distance == 0f) { 612 characterRange[0] = currentLineStart; 613 characterRange[1] = currentLineEnd; 614 if (bounds != null) { 615 bounds.set(currentLineBounds); 616 } 617 return; 618 } 619 620 if (distance < minDistance) { 621 minDistance = distance; 622 characterRange[0] = currentLineStart; 623 characterRange[1] = currentLineEnd; 624 if (bounds != null) { 625 bounds.set(currentLineBounds); 626 } 627 } 628 if (y < bounds.top) break; 629 currentLineStart = mLineSegmentFinder.nextStartBoundary(currentLineStart); 630 currentLineEnd = mLineSegmentFinder.nextEndBoundary(currentLineEnd); 631 } 632 633 // y is above the first line or below the last line. The founded line is still invalid, 634 // clear the result. 635 if (y < top || y > bottom) { 636 characterRange[0] = -1; 637 characterRange[1] = -1; 638 if (bounds != null) { 639 bounds.setEmpty(); 640 } 641 } 642 } 643 644 /** 645 * Finds the range of text which is inside the specified rectangle area. This method is a 646 * counterpart of the 647 * {@link Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)}. 648 * 649 * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom 650 * and each line is laid out according to the display algorithm specified in 651 * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional 652 * algorithm</a>. 653 * </p> 654 * 655 * <p> This method won't check the text ranges whose line information is missing. For example, 656 * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated line 657 * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method 658 * won't check the text in the ranges of [5, 7) and [12, 15). 659 * </p> 660 * 661 * @param area area for which the text range will be found 662 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 663 * text segment 664 * @param inclusionStrategy strategy for determining whether a text segment is inside the 665 * specified area 666 * @return the text range stored in a two element int array. The first element is the 667 * start (inclusive) of the text range, and the second element is the end (exclusive) character 668 * offsets of the text range, or null if there are no text segments inside the area. 669 * 670 * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy) 671 */ 672 @Nullable getRangeForRect(@onNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)673 public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder, 674 @NonNull Layout.TextInclusionStrategy inclusionStrategy) { 675 int lineEnd = mLineSegmentFinder.nextEndBoundary(mStart); 676 // Line information is missing. 677 if (lineEnd == SegmentFinder.DONE) return null; 678 int lineStart = mLineSegmentFinder.previousStartBoundary(lineEnd); 679 680 int start = -1; 681 while (lineStart != SegmentFinder.DONE && start == -1) { 682 start = getStartForRectWithinLine(lineStart, lineEnd, area, segmentFinder, 683 inclusionStrategy); 684 lineStart = mLineSegmentFinder.nextStartBoundary(lineStart); 685 lineEnd = mLineSegmentFinder.nextEndBoundary(lineEnd); 686 } 687 688 // Can't find the start index; the specified contains no valid segment. 689 if (start == -1) return null; 690 691 lineStart = mLineSegmentFinder.previousStartBoundary(mEnd); 692 // Line information is missing. 693 if (lineStart == SegmentFinder.DONE) return null; 694 lineEnd = mLineSegmentFinder.nextEndBoundary(lineStart); 695 int end = -1; 696 while (lineEnd > start && end == -1) { 697 end = getEndForRectWithinLine(lineStart, lineEnd, area, segmentFinder, 698 inclusionStrategy); 699 lineStart = mLineSegmentFinder.previousStartBoundary(lineStart); 700 lineEnd = mLineSegmentFinder.previousEndBoundary(lineEnd); 701 } 702 703 // We've already found start, end is guaranteed to be found at this point. 704 start = segmentFinder.previousStartBoundary(start + 1); 705 end = segmentFinder.nextEndBoundary(end - 1); 706 return new int[] { start, end }; 707 } 708 709 /** 710 * Find the start character index of the first text segments within a line inside the specified 711 * {@code area}. 712 * 713 * @param lineStart the start of this line, inclusive . 714 * @param lineEnd the end of this line, exclusive. 715 * @param area the area inside which the text segments will be found. 716 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a 717 * text segment. 718 * @param inclusionStrategy strategy for determining whether a text segment is inside the 719 * specified area. 720 * @return the start index of the first segment in the area. 721 */ getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)722 private int getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area, 723 @NonNull SegmentFinder segmentFinder, 724 @NonNull Layout.TextInclusionStrategy inclusionStrategy) { 725 if (lineStart >= lineEnd) return -1; 726 727 int runStart = lineStart; 728 int runLevel = -1; 729 // Check the BiDi runs and search for the start index. 730 for (int index = lineStart; index < lineEnd; ++index) { 731 final int level = getCharacterBidiLevel(index); 732 if (level != runLevel) { 733 final int start = getStartForRectWithinRun(runStart, index, area, segmentFinder, 734 inclusionStrategy); 735 if (start != -1) { 736 return start; 737 } 738 739 runStart = index; 740 runLevel = level; 741 } 742 } 743 return getStartForRectWithinRun(runStart, lineEnd, area, segmentFinder, inclusionStrategy); 744 } 745 746 /** 747 * Find the start character index of the first text segments within the directional run inside 748 * the specified {@code area}. 749 * 750 * @param runStart the start of this directional run, inclusive. 751 * @param runEnd the end of this directional run, exclusive. 752 * @param area the area inside which the text segments will be found. 753 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a 754 * text segment. 755 * @param inclusionStrategy strategy for determining whether a text segment is inside the 756 * specified area. 757 * @return the start index of the first segment in the area. 758 */ getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)759 private int getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area, 760 @NonNull SegmentFinder segmentFinder, 761 @NonNull Layout.TextInclusionStrategy inclusionStrategy) { 762 if (runStart >= runEnd) return -1; 763 764 int segmentEndOffset = segmentFinder.nextEndBoundary(runStart); 765 // No segment is found in run. 766 if (segmentEndOffset == SegmentFinder.DONE) return -1; 767 int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset); 768 769 final RectF segmentBounds = new RectF(); 770 while (segmentStartOffset != SegmentFinder.DONE && segmentStartOffset < runEnd) { 771 final int start = Math.max(runStart, segmentStartOffset); 772 final int end = Math.min(runEnd, segmentEndOffset); 773 getBoundsForRange(start, end, segmentBounds); 774 // Find the first segment inside the area, return the start. 775 if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return start; 776 777 segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset); 778 segmentEndOffset = segmentFinder.nextEndBoundary(segmentEndOffset); 779 } 780 return -1; 781 } 782 783 /** 784 * Find the end character index of the last text segments within a line inside the specified 785 * {@code area}. 786 * 787 * @param lineStart the start of this line, inclusive . 788 * @param lineEnd the end of this line, exclusive. 789 * @param area the area inside which the text segments will be found. 790 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a 791 * text segment. 792 * @param inclusionStrategy strategy for determining whether a text segment is inside the 793 * specified area. 794 * @return the end index of the last segment in the area. 795 */ getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)796 private int getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area, 797 @NonNull SegmentFinder segmentFinder, 798 @NonNull Layout.TextInclusionStrategy inclusionStrategy) { 799 if (lineStart >= lineEnd) return -1; 800 lineStart = Math.max(lineStart, mStart); 801 lineEnd = Math.min(lineEnd, mEnd); 802 803 // The exclusive run end index. 804 int runEnd = lineEnd; 805 int runLevel = -1; 806 // Check the BiDi runs backwards and search for the end index. 807 for (int index = lineEnd - 1; index >= lineStart; --index) { 808 final int level = getCharacterBidiLevel(index); 809 if (level != runLevel) { 810 final int end = getEndForRectWithinRun(index + 1, runEnd, area, segmentFinder, 811 inclusionStrategy); 812 if (end != -1) return end; 813 814 runEnd = index + 1; 815 runLevel = level; 816 } 817 } 818 return getEndForRectWithinRun(lineStart, runEnd, area, segmentFinder, inclusionStrategy); 819 } 820 821 /** 822 * Find the end character index of the last text segments within the directional run inside the 823 * specified {@code area}. 824 * 825 * @param runStart the start of this directional run, inclusive. 826 * @param runEnd the end of this directional run, exclusive. 827 * @param area the area inside which the text segments will be found. 828 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a 829 * text segment. 830 * @param inclusionStrategy strategy for determining whether a text segment is inside the 831 * specified area. 832 * @return the end index of the last segment in the area. 833 */ getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)834 private int getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area, 835 @NonNull SegmentFinder segmentFinder, 836 @NonNull Layout.TextInclusionStrategy inclusionStrategy) { 837 if (runStart >= runEnd) return -1; 838 839 int segmentStart = segmentFinder.previousStartBoundary(runEnd); 840 // No segment is found before the runEnd. 841 if (segmentStart == SegmentFinder.DONE) return -1; 842 int segmentEnd = segmentFinder.nextEndBoundary(segmentStart); 843 844 final RectF segmentBounds = new RectF(); 845 while (segmentEnd != SegmentFinder.DONE && segmentEnd > runStart) { 846 final int start = Math.max(runStart, segmentStart); 847 final int end = Math.min(runEnd, segmentEnd); 848 getBoundsForRange(start, end, segmentBounds); 849 // Find the last segment inside the area, return the end. 850 if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return end; 851 852 segmentStart = segmentFinder.previousStartBoundary(segmentStart); 853 segmentEnd = segmentFinder.previousEndBoundary(segmentEnd); 854 } 855 return -1; 856 } 857 858 /** 859 * Get the vertical distance from the {@code pointF} to the {@code rectF}. It's useful to find 860 * the corresponding line for a given point. 861 */ verticalDistance(@onNull RectF rectF, float y)862 private static float verticalDistance(@NonNull RectF rectF, float y) { 863 if (rectF.top <= y && y < rectF.bottom) { 864 return 0f; 865 } 866 if (y < rectF.top) { 867 return rectF.top - y; 868 } 869 return y - rectF.bottom; 870 } 871 872 /** 873 * Describe the kinds of special objects contained in this Parcelable 874 * instance's marshaled representation. For example, if the object will 875 * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)}, 876 * the return value of this method must include the 877 * {@link #CONTENTS_FILE_DESCRIPTOR} bit. 878 * 879 * @return a bitmask indicating the set of special object types marshaled 880 * by this Parcelable object instance. 881 */ 882 @Override describeContents()883 public int describeContents() { 884 return 0; 885 } 886 887 /** 888 * Flatten this object in to a Parcel. 889 * 890 * @param dest The Parcel in which the object should be written. 891 * @param flags Additional flags about how the object should be written. 892 * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. 893 */ 894 @Override writeToParcel(@onNull Parcel dest, int flags)895 public void writeToParcel(@NonNull Parcel dest, int flags) { 896 dest.writeInt(mStart); 897 dest.writeInt(mEnd); 898 dest.writeFloatArray(mMatrixValues); 899 dest.writeFloatArray(mCharacterBounds); 900 901 // The end can also be a break position. We need an extra space to encode the breaks. 902 final int[] encodedFlags = Arrays.copyOf(mInternalCharacterFlags, mEnd - mStart + 1); 903 encodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, FLAG_GRAPHEME_SEGMENT_END, 904 mStart, mEnd, mGraphemeSegmentFinder); 905 encodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, FLAG_WORD_SEGMENT_END, mStart, 906 mEnd, mWordSegmentFinder); 907 encodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, FLAG_LINE_SEGMENT_END, mStart, 908 mEnd, mLineSegmentFinder); 909 dest.writeIntArray(encodedFlags); 910 } 911 TextBoundsInfo(Parcel source)912 private TextBoundsInfo(Parcel source) { 913 mStart = source.readInt(); 914 mEnd = source.readInt(); 915 mMatrixValues = Objects.requireNonNull(source.createFloatArray()); 916 mCharacterBounds = Objects.requireNonNull(source.createFloatArray()); 917 final int[] encodedFlags = Objects.requireNonNull(source.createIntArray()); 918 919 mGraphemeSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, 920 FLAG_GRAPHEME_SEGMENT_END, mStart, mEnd); 921 mWordSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, 922 FLAG_WORD_SEGMENT_END, mStart, mEnd); 923 mLineSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, 924 FLAG_LINE_SEGMENT_END, mStart, mEnd); 925 926 final int length = mEnd - mStart; 927 final int flagsMask = KNOWN_CHARACTER_FLAGS | BIDI_LEVEL_MASK; 928 mInternalCharacterFlags = new int[length]; 929 for (int i = 0; i < length; ++i) { 930 // Remove the flags used to encoded segment boundaries. 931 mInternalCharacterFlags[i] = encodedFlags[i] & flagsMask; 932 } 933 } 934 TextBoundsInfo(Builder builder)935 private TextBoundsInfo(Builder builder) { 936 mStart = builder.mStart; 937 mEnd = builder.mEnd; 938 mMatrixValues = Arrays.copyOf(builder.mMatrixValues, 9); 939 final int length = mEnd - mStart; 940 mCharacterBounds = Arrays.copyOf(builder.mCharacterBounds, 4 * length); 941 // Store characterFlags and characterBidiLevels to save memory. 942 mInternalCharacterFlags = new int[length]; 943 for (int index = 0; index < length; ++index) { 944 mInternalCharacterFlags[index] = builder.mCharacterFlags[index] 945 | (builder.mCharacterBidiLevels[index] << BIDI_LEVEL_SHIFT); 946 } 947 mGraphemeSegmentFinder = builder.mGraphemeSegmentFinder; 948 mWordSegmentFinder = builder.mWordSegmentFinder; 949 mLineSegmentFinder = builder.mLineSegmentFinder; 950 } 951 952 /** 953 * The CREATOR to make this class Parcelable. 954 */ 955 @NonNull 956 public static final Parcelable.Creator<TextBoundsInfo> CREATOR = new Creator<TextBoundsInfo>() { 957 @Override 958 public TextBoundsInfo createFromParcel(Parcel source) { 959 return new TextBoundsInfo(source); 960 } 961 962 @Override 963 public TextBoundsInfo[] newArray(int size) { 964 return new TextBoundsInfo[size]; 965 } 966 }; 967 968 private static final String TEXT_BOUNDS_INFO_KEY = "android.view.inputmethod.TextBoundsInfo"; 969 970 /** 971 * Store the {@link TextBoundsInfo} into a {@link Bundle}. This method is used by 972 * {@link RemoteInputConnectionImpl} to transfer the {@link TextBoundsInfo} from the editor 973 * to IME. 974 * 975 * @see TextBoundsInfoResult 976 * @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer) 977 * @hide 978 */ 979 @NonNull toBundle()980 public Bundle toBundle() { 981 final Bundle bundle = new Bundle(); 982 bundle.putParcelable(TEXT_BOUNDS_INFO_KEY, this); 983 return bundle; 984 985 } 986 987 /** @hide */ 988 @Nullable createFromBundle(@ullable Bundle bundle)989 public static TextBoundsInfo createFromBundle(@Nullable Bundle bundle) { 990 if (bundle == null) return null; 991 return bundle.getParcelable(TEXT_BOUNDS_INFO_KEY, TextBoundsInfo.class); 992 } 993 994 /** 995 * The builder class to create a {@link TextBoundsInfo} object. 996 */ 997 public static final class Builder { 998 private final float[] mMatrixValues = new float[9]; 999 private boolean mMatrixInitialized; 1000 private int mStart = -1; 1001 private int mEnd = -1; 1002 private float[] mCharacterBounds; 1003 private int[] mCharacterFlags; 1004 private int[] mCharacterBidiLevels; 1005 private SegmentFinder mLineSegmentFinder; 1006 private SegmentFinder mWordSegmentFinder; 1007 private SegmentFinder mGraphemeSegmentFinder; 1008 1009 /** 1010 * Create a builder for {@link TextBoundsInfo}. 1011 * @param start the start index of the {@link TextBoundsInfo}, inclusive. 1012 * @param end the end index of the {@link TextBoundsInfo}, exclusive. 1013 * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative, 1014 * or {@code end} is smaller than the {@code start}. 1015 */ Builder(int start, int end)1016 public Builder(int start, int end) { 1017 setStartAndEnd(start, end); 1018 } 1019 1020 /** Clear all the parameters set on this {@link Builder} to reuse it. */ 1021 @NonNull clear()1022 public Builder clear() { 1023 mMatrixInitialized = false; 1024 mStart = -1; 1025 mEnd = -1; 1026 mCharacterBounds = null; 1027 mCharacterFlags = null; 1028 mCharacterBidiLevels = null; 1029 mLineSegmentFinder = null; 1030 mWordSegmentFinder = null; 1031 mGraphemeSegmentFinder = null; 1032 return this; 1033 } 1034 1035 /** 1036 * Sets the matrix that transforms local coordinates into screen coordinates. 1037 * 1038 * @param matrix transformation matrix from local coordinates into screen coordinates. 1039 * @throws NullPointerException if the given {@code matrix} is {@code null}. 1040 */ 1041 @NonNull setMatrix(@onNull Matrix matrix)1042 public Builder setMatrix(@NonNull Matrix matrix) { 1043 Objects.requireNonNull(matrix).getValues(mMatrixValues); 1044 mMatrixInitialized = true; 1045 return this; 1046 } 1047 1048 /** 1049 * Set the start and end index of the {@link TextBoundsInfo}. It's the range of the 1050 * characters whose information is available in the {@link TextBoundsInfo}. 1051 * 1052 * @param start the start index of the {@link TextBoundsInfo}, inclusive. 1053 * @param end the end index of the {@link TextBoundsInfo}, exclusive. 1054 * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative, 1055 * or {@code end} is smaller than the {@code start}. 1056 */ 1057 @NonNull 1058 @SuppressWarnings("MissingGetterMatchingBuilder") setStartAndEnd(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)1059 public Builder setStartAndEnd(@IntRange(from = 0) int start, @IntRange(from = 0) int end) { 1060 Preconditions.checkArgument(start >= 0); 1061 Preconditions.checkArgumentInRange(start, 0, end, "start"); 1062 mStart = start; 1063 mEnd = end; 1064 return this; 1065 } 1066 1067 /** 1068 * Set the characters bounds, in the coordinates of the editor. <br/> 1069 * 1070 * The given array should be divided into groups of four where each element represents 1071 * left, top, right and bottom of the character bounds respectively. 1072 * The bounds of the i-th character in the editor should be stored at index 1073 * 4 * (i - start). The length of the given array must equal to 4 * (end - start). <br/> 1074 * 1075 * Sometimes multiple characters in a single grapheme are rendered as one symbol on the 1076 * screen. So those characters only have one shared bounds. In this case, we recommend the 1077 * editor to assign all the width to the bounds of the first character in the grapheme, 1078 * and make the rest characters' bounds zero-width. <br/> 1079 * 1080 * For example, the string "'0xD83D' '0xDE00'" is rendered as one grapheme - a grinning face 1081 * emoji. If the bounds of the grapheme is: Rect(5, 10, 15, 20), the character bounds of the 1082 * string should be: [ Rect(5, 10, 15, 20), Rect(15, 10, 15, 20) ]. 1083 * 1084 * @param characterBounds the array of the flattened character bounds. 1085 * @throws NullPointerException if the given {@code characterBounds} is {@code null}. 1086 */ 1087 @NonNull setCharacterBounds(@onNull float[] characterBounds)1088 public Builder setCharacterBounds(@NonNull float[] characterBounds) { 1089 mCharacterBounds = Objects.requireNonNull(characterBounds); 1090 return this; 1091 } 1092 1093 /** 1094 * Set the flags of the characters. The flags of the i-th character in the editor is stored 1095 * at index (i - start). The length of the given array must equal to (end - start). 1096 * The flags contain the following information: 1097 * <ul> 1098 * <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a 1099 * whitespace. </li> 1100 * <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a 1101 * linefeed. </li> 1102 * <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a 1103 * punctuation. </li> 1104 * <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to 1105 * is RTL. All all character in the same line must have the same line direction. Check 1106 * {@link #getLineSegmentFinder()} for more information of line boundaries. </li> 1107 * </ul> 1108 * 1109 * @param characterFlags the array of the character's flags. 1110 * @throws NullPointerException if the given {@code characterFlags} is {@code null}. 1111 * @throws IllegalArgumentException if the given {@code characterFlags} contains invalid 1112 * flags. 1113 * 1114 * @see #getCharacterFlags(int) 1115 */ 1116 @NonNull setCharacterFlags(@onNull int[] characterFlags)1117 public Builder setCharacterFlags(@NonNull int[] characterFlags) { 1118 Objects.requireNonNull(characterFlags); 1119 for (int characterFlag : characterFlags) { 1120 if ((characterFlag & (~KNOWN_CHARACTER_FLAGS)) != 0) { 1121 throw new IllegalArgumentException("characterFlags contains invalid flags."); 1122 } 1123 } 1124 mCharacterFlags = characterFlags; 1125 return this; 1126 } 1127 1128 /** 1129 * Set the BiDi levels for the character. The bidiLevel of the i-th character in the editor 1130 * is stored at index (i - start). The length of the given array must equal to 1131 * (end - start). <br/> 1132 * 1133 * BiDi level is defined by 1134 * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode 1135 * bidirectional algorithm </a>. One can determine whether a character's direction is 1136 * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level. 1137 * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a 1138 * character must be in the range of [0, 125]. 1139 * @param characterBidiLevels the array of the character's BiDi level. 1140 * 1141 * @throws NullPointerException if the given {@code characterBidiLevels} is {@code null}. 1142 * @throws IllegalArgumentException if the given {@code characterBidiLevels} contains an 1143 * element that's out of the range [0, 125]. 1144 * 1145 * @see #getCharacterBidiLevel(int) 1146 */ 1147 @NonNull setCharacterBidiLevel(@onNull int[] characterBidiLevels)1148 public Builder setCharacterBidiLevel(@NonNull int[] characterBidiLevels) { 1149 Objects.requireNonNull(characterBidiLevels); 1150 for (int index = 0; index < characterBidiLevels.length; ++index) { 1151 Preconditions.checkArgumentInRange(characterBidiLevels[index], 0, 125, 1152 "bidiLevels[" + index + "]"); 1153 } 1154 mCharacterBidiLevels = characterBidiLevels; 1155 return this; 1156 } 1157 1158 /** 1159 * Set the {@link SegmentFinder} that locates the grapheme cluster boundaries. Grapheme is 1160 * defined in <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries"> 1161 * the unicode annex #29: unicode text segmentation<a/>. It's a user-perspective character. 1162 * And it's usually the minimal unit for selection, backspace, deletion etc. <br/> 1163 * 1164 * Please note that only the grapheme segments within the range from start to end will 1165 * be available to the IME. The remaining information will be discarded during serialization 1166 * for better performance. 1167 * 1168 * @param graphemeSegmentFinder the {@link SegmentFinder} that locates the grapheme cluster 1169 * boundaries. 1170 * @throws NullPointerException if the given {@code graphemeSegmentFinder} is {@code null}. 1171 * 1172 * @see #getGraphemeSegmentFinder() 1173 * @see SegmentFinder 1174 * @see SegmentFinder.PrescribedSegmentFinder 1175 */ 1176 @NonNull setGraphemeSegmentFinder(@onNull SegmentFinder graphemeSegmentFinder)1177 public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) { 1178 mGraphemeSegmentFinder = Objects.requireNonNull(graphemeSegmentFinder); 1179 return this; 1180 } 1181 1182 /** 1183 * Set the {@link SegmentFinder} that locates the word boundaries. <br/> 1184 * 1185 * Please note that only the word segments within the range from start to end will 1186 * be available to the IME. The remaining information will be discarded during serialization 1187 * for better performance. 1188 * @param wordSegmentFinder set the {@link SegmentFinder} that locates the word boundaries. 1189 * @throws NullPointerException if the given {@code wordSegmentFinder} is {@code null}. 1190 * 1191 * @see #getWordSegmentFinder() 1192 * @see SegmentFinder 1193 * @see SegmentFinder.PrescribedSegmentFinder 1194 */ 1195 @NonNull setWordSegmentFinder(@onNull SegmentFinder wordSegmentFinder)1196 public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) { 1197 mWordSegmentFinder = Objects.requireNonNull(wordSegmentFinder); 1198 return this; 1199 } 1200 1201 /** 1202 * Set the {@link SegmentFinder} that locates the line boundaries. Aside from the hard 1203 * breaks in the text, it should also locate the soft line breaks added by the editor. 1204 * It is expected that the characters within the same line is rendered on the same baseline. 1205 * (Except for some text formatted as subscript and superscript.) <br/> 1206 * 1207 * Please note that only the line segments within the range from start to end will 1208 * be available to the IME. The remaining information will be discarded during serialization 1209 * for better performance. 1210 * @param lineSegmentFinder set the {@link SegmentFinder} that locates the line boundaries. 1211 * @throws NullPointerException if the given {@code lineSegmentFinder} is {@code null}. 1212 * 1213 * @see #getLineSegmentFinder() 1214 * @see SegmentFinder 1215 * @see SegmentFinder.PrescribedSegmentFinder 1216 */ 1217 @NonNull setLineSegmentFinder(@onNull SegmentFinder lineSegmentFinder)1218 public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) { 1219 mLineSegmentFinder = Objects.requireNonNull(lineSegmentFinder); 1220 return this; 1221 } 1222 1223 /** 1224 * Create the {@link TextBoundsInfo} using the parameters in this {@link Builder}. 1225 * 1226 * @throws IllegalStateException in the following conditions: 1227 * <ul> 1228 * <li>if the {@code start} or {@code end} is not set.</li> 1229 * <li>if the {@code matrix} is not set.</li> 1230 * <li>if {@code characterBounds} is not set or its length doesn't equal to 1231 * 4 * ({@code end} - {@code start}).</li> 1232 * <li>if the {@code characterFlags} is not set or its length doesn't equal to 1233 * ({@code end} - {@code start}).</li> 1234 * <li>if {@code graphemeSegmentFinder}, {@code wordSegmentFinder} or 1235 * {@code lineSegmentFinder} is not set.</li> 1236 * <li>if characters in the same line has inconsistent {@link #FLAG_LINE_IS_RTL} 1237 * flag.</li> 1238 * </ul> 1239 */ 1240 @NonNull build()1241 public TextBoundsInfo build() { 1242 if (mStart < 0 || mEnd < 0) { 1243 throw new IllegalStateException("Start and end must be set."); 1244 } 1245 1246 if (!mMatrixInitialized) { 1247 throw new IllegalStateException("Matrix must be set."); 1248 } 1249 1250 if (mCharacterBounds == null) { 1251 throw new IllegalStateException("CharacterBounds must be set."); 1252 } 1253 1254 if (mCharacterFlags == null) { 1255 throw new IllegalStateException("CharacterFlags must be set."); 1256 } 1257 1258 if (mCharacterBidiLevels == null) { 1259 throw new IllegalStateException("CharacterBidiLevel must be set."); 1260 } 1261 1262 if (mCharacterBounds.length != 4 * (mEnd - mStart)) { 1263 throw new IllegalStateException("The length of characterBounds doesn't match the " 1264 + "length of the given start and end." 1265 + " Expected length: " + (4 * (mEnd - mStart)) 1266 + " characterBounds length: " + mCharacterBounds.length); 1267 } 1268 if (mCharacterFlags.length != mEnd - mStart) { 1269 throw new IllegalStateException("The length of characterFlags doesn't match the " 1270 + "length of the given start and end." 1271 + " Expected length: " + (mEnd - mStart) 1272 + " characterFlags length: " + mCharacterFlags.length); 1273 } 1274 if (mCharacterBidiLevels.length != mEnd - mStart) { 1275 throw new IllegalStateException("The length of characterBidiLevels doesn't match" 1276 + " the length of the given start and end." 1277 + " Expected length: " + (mEnd - mStart) 1278 + " characterFlags length: " + mCharacterBidiLevels.length); 1279 } 1280 if (mGraphemeSegmentFinder == null) { 1281 throw new IllegalStateException("GraphemeSegmentFinder must be set."); 1282 } 1283 if (mWordSegmentFinder == null) { 1284 throw new IllegalStateException("WordSegmentFinder must be set."); 1285 } 1286 if (mLineSegmentFinder == null) { 1287 throw new IllegalStateException("LineSegmentFinder must be set."); 1288 } 1289 1290 if (!isLineDirectionFlagConsistent(mCharacterFlags, mLineSegmentFinder, mStart, mEnd)) { 1291 throw new IllegalStateException("characters in the same line must have the same " 1292 + "FLAG_LINE_IS_RTL flag value."); 1293 } 1294 return new TextBoundsInfo(this); 1295 } 1296 } 1297 1298 /** 1299 * Encode the segment start and end positions in {@link SegmentFinder} to a flags array. 1300 * 1301 * For example: 1302 * Text: "A BC DE" 1303 * Input: 1304 * start: 2, end: 7 // substring "BC DE" 1305 * SegmentFinder: segment ranges = [(2, 4), (5, 7)] // a word break iterator 1306 * flags: [0x0000, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000] // 0x0080 is whitespace 1307 * segmentStartFlag: 0x0100 1308 * segmentEndFlag: 0x0200 1309 * Output: 1310 * flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200] 1311 * The index 2 and 5 encode segment starts, the index 4 and 7 encode a segment end. 1312 * 1313 * @param flags the flags array to receive the results. 1314 * @param segmentStartFlag the flag used to encode the segment start. 1315 * @param segmentEndFlag the flag used to encode the segment end. 1316 * @param start the start index of the encoded range, inclusive. 1317 * @param end the end index of the encoded range, inclusive. 1318 * @param segmentFinder the SegmentFinder to be encoded. 1319 * 1320 * @see #decodeSegmentFinder(int[], int, int, int, int) 1321 */ encodeSegmentFinder(@onNull int[] flags, int segmentStartFlag, int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder)1322 private static void encodeSegmentFinder(@NonNull int[] flags, int segmentStartFlag, 1323 int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder) { 1324 if (end - start + 1 != flags.length) { 1325 throw new IllegalStateException("The given flags array must have the same length as" 1326 + " the given range. flags length: " + flags.length 1327 + " range: [" + start + ", " + end + "]"); 1328 } 1329 1330 int segmentEnd = segmentFinder.nextEndBoundary(start); 1331 if (segmentEnd == SegmentFinder.DONE) return; 1332 int segmentStart = segmentFinder.previousStartBoundary(segmentEnd); 1333 1334 while (segmentEnd != SegmentFinder.DONE && segmentEnd <= end) { 1335 if (segmentStart >= start) { 1336 flags[segmentStart - start] |= segmentStartFlag; 1337 flags[segmentEnd - start] |= segmentEndFlag; 1338 } 1339 segmentStart = segmentFinder.nextStartBoundary(segmentStart); 1340 segmentEnd = segmentFinder.nextEndBoundary(segmentEnd); 1341 } 1342 } 1343 1344 /** 1345 * Decode a {@link SegmentFinder} from a flags array. 1346 * 1347 * For example: 1348 * Text: "A BC DE" 1349 * Input: 1350 * start: 2, end: 7 // substring "BC DE" 1351 * flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200] 1352 * segmentStartFlag: 0x0100 1353 * segmentEndFlag: 0x0200 1354 * Output: 1355 * SegmentFinder: segment ranges = [(2, 4), (5, 7)] 1356 * 1357 * @param flags the flags array to decode the SegmentFinder. 1358 * @param segmentStartFlag the flag to decode a segment start. 1359 * @param segmentEndFlag the flag to decode a segment end. 1360 * @param start the start index of the interested range, inclusive. 1361 * @param end the end index of the interested range, inclusive. 1362 * 1363 * @see #encodeSegmentFinder(int[], int, int, int, int, SegmentFinder) 1364 */ decodeSegmentFinder(int[] flags, int segmentStartFlag, int segmentEndFlag, int start, int end)1365 private static SegmentFinder decodeSegmentFinder(int[] flags, int segmentStartFlag, 1366 int segmentEndFlag, int start, int end) { 1367 if (end - start + 1 != flags.length) { 1368 throw new IllegalStateException("The given flags array must have the same length as" 1369 + " the given range. flags length: " + flags.length 1370 + " range: [" + start + ", " + end + "]"); 1371 } 1372 int[] breaks = ArrayUtils.newUnpaddedIntArray(10); 1373 int count = 0; 1374 for (int offset = 0; offset < flags.length; ++offset) { 1375 if ((flags[offset] & segmentStartFlag) == segmentStartFlag) { 1376 breaks = GrowingArrayUtils.append(breaks, count++, start + offset); 1377 } 1378 if ((flags[offset] & segmentEndFlag) == segmentEndFlag) { 1379 breaks = GrowingArrayUtils.append(breaks, count++, start + offset); 1380 } 1381 } 1382 return new SegmentFinder.PrescribedSegmentFinder(Arrays.copyOf(breaks, count)); 1383 } 1384 1385 /** 1386 * Check whether the {@link #FLAG_LINE_IS_RTL} is the same for characters in the same line. 1387 * @return true if all characters in the same line has the same {@link #FLAG_LINE_IS_RTL} flag. 1388 */ isLineDirectionFlagConsistent(int[] characterFlags, SegmentFinder lineSegmentFinder, int start, int end)1389 private static boolean isLineDirectionFlagConsistent(int[] characterFlags, 1390 SegmentFinder lineSegmentFinder, int start, int end) { 1391 int segmentEnd = lineSegmentFinder.nextEndBoundary(start); 1392 if (segmentEnd == SegmentFinder.DONE) return true; 1393 int segmentStart = lineSegmentFinder.previousStartBoundary(segmentEnd); 1394 1395 while (segmentStart != SegmentFinder.DONE && segmentStart < end) { 1396 final int lineStart = Math.max(segmentStart, start); 1397 final int lineEnd = Math.min(segmentEnd, end); 1398 final boolean lineIsRtl = (characterFlags[lineStart - start] & FLAG_LINE_IS_RTL) != 0; 1399 for (int index = lineStart + 1; index < lineEnd; ++index) { 1400 final int flags = characterFlags[index - start]; 1401 final boolean characterLineIsRtl = (flags & FLAG_LINE_IS_RTL) != 0; 1402 if (characterLineIsRtl != lineIsRtl) { 1403 return false; 1404 } 1405 } 1406 1407 segmentStart = lineSegmentFinder.nextStartBoundary(segmentStart); 1408 segmentEnd = lineSegmentFinder.nextEndBoundary(segmentEnd); 1409 } 1410 return true; 1411 } 1412 } 1413