CursorAnchorInfo.java revision cc24e2b6a2a429d70b75c6810a5cfd8816ce03ad
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package android.view.inputmethod; 18 19import android.graphics.Matrix; 20import android.graphics.RectF; 21import android.os.Parcel; 22import android.os.Parcelable; 23import android.text.Layout; 24import android.text.SpannedString; 25import android.text.TextUtils; 26import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder; 27 28import java.util.Objects; 29 30/** 31 * Positional information about the text insertion point and characters in the composition string. 32 * 33 * <p>This class encapsulates locations of the text insertion point and the composition string in 34 * the screen coordinates so that IMEs can render their UI components near where the text is 35 * actually inserted.</p> 36 */ 37public final class CursorAnchorInfo implements Parcelable { 38 private final int mSelectionStart; 39 private final int mSelectionEnd; 40 41 private final int mComposingTextStart; 42 /** 43 * The text, tracked as a composing region. 44 */ 45 private final CharSequence mComposingText; 46 47 /** 48 * Flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for example. 49 */ 50 private final int mInsertionMarkerFlags; 51 /** 52 * Horizontal position of the insertion marker, in the local coordinates that will be 53 * transformed with the transformation matrix when rendered on the screen. This should be 54 * calculated or compatible with {@link Layout#getPrimaryHorizontal(int)}. This can be 55 * {@code java.lang.Float.NaN} when no value is specified. 56 */ 57 private final float mInsertionMarkerHorizontal; 58 /** 59 * Vertical position of the insertion marker, in the local coordinates that will be 60 * transformed with the transformation matrix when rendered on the screen. This should be 61 * calculated or compatible with {@link Layout#getLineTop(int)}. This can be 62 * {@code java.lang.Float.NaN} when no value is specified. 63 */ 64 private final float mInsertionMarkerTop; 65 /** 66 * Vertical position of the insertion marker, in the local coordinates that will be 67 * transformed with the transformation matrix when rendered on the screen. This should be 68 * calculated or compatible with {@link Layout#getLineBaseline(int)}. This can be 69 * {@code java.lang.Float.NaN} when no value is specified. 70 */ 71 private final float mInsertionMarkerBaseline; 72 /** 73 * Vertical position of the insertion marker, in the local coordinates that will be 74 * transformed with the transformation matrix when rendered on the screen. This should be 75 * calculated or compatible with {@link Layout#getLineBottom(int)}. This can be 76 * {@code java.lang.Float.NaN} when no value is specified. 77 */ 78 private final float mInsertionMarkerBottom; 79 80 /** 81 * Container of rectangular position of characters, keyed with character index in a unit of 82 * Java chars, in the local coordinates that will be transformed with the transformation matrix 83 * when rendered on the screen. 84 */ 85 private final SparseRectFArray mCharacterRects; 86 87 /** 88 * Transformation matrix that is applied to any positional information of this class to 89 * transform local coordinates into screen coordinates. 90 */ 91 private final Matrix mMatrix; 92 93 /** 94 * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterRectFlags(int)}: the 95 * insertion marker or character bounds have at least one visible region. 96 */ 97 public static final int FLAG_HAS_VISIBLE_REGION = 0x01; 98 99 /** 100 * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterRectFlags(int)}: the 101 * insertion marker or character bounds have at least one invisible (clipped) region. 102 */ 103 public static final int FLAG_HAS_INVISIBLE_REGION = 0x02; 104 105 /** 106 * @removed 107 */ 108 public static final int CHARACTER_RECT_TYPE_MASK = 0x0f; 109 /** 110 * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the editor did not specify any type of this 111 * character. Editor authors should not use this flag. 112 * @removed 113 */ 114 public static final int CHARACTER_RECT_TYPE_UNSPECIFIED = 0; 115 /** 116 * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the character is entirely visible. 117 * @removed 118 */ 119 public static final int CHARACTER_RECT_TYPE_FULLY_VISIBLE = 1; 120 /** 121 * Type for {@link #CHARACTER_RECT_TYPE_MASK}: some area of the character is invisible. 122 * @removed 123 */ 124 public static final int CHARACTER_RECT_TYPE_PARTIALLY_VISIBLE = 2; 125 /** 126 * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the character is entirely invisible. 127 * @removed 128 */ 129 public static final int CHARACTER_RECT_TYPE_INVISIBLE = 3; 130 /** 131 * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the editor gave up to calculate the rectangle 132 * for this character. Input method authors should ignore the returned rectangle. 133 * @removed 134 */ 135 public static final int CHARACTER_RECT_TYPE_NOT_FEASIBLE = 4; 136 137 public CursorAnchorInfo(final Parcel source) { 138 mSelectionStart = source.readInt(); 139 mSelectionEnd = source.readInt(); 140 mComposingTextStart = source.readInt(); 141 mComposingText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); 142 mInsertionMarkerFlags = source.readInt(); 143 mInsertionMarkerHorizontal = source.readFloat(); 144 mInsertionMarkerTop = source.readFloat(); 145 mInsertionMarkerBaseline = source.readFloat(); 146 mInsertionMarkerBottom = source.readFloat(); 147 mCharacterRects = source.readParcelable(SparseRectFArray.class.getClassLoader()); 148 mMatrix = new Matrix(); 149 mMatrix.setValues(source.createFloatArray()); 150 } 151 152 /** 153 * Used to package this object into a {@link Parcel}. 154 * 155 * @param dest The {@link Parcel} to be written. 156 * @param flags The flags used for parceling. 157 */ 158 @Override 159 public void writeToParcel(Parcel dest, int flags) { 160 dest.writeInt(mSelectionStart); 161 dest.writeInt(mSelectionEnd); 162 dest.writeInt(mComposingTextStart); 163 TextUtils.writeToParcel(mComposingText, dest, flags); 164 dest.writeInt(mInsertionMarkerFlags); 165 dest.writeFloat(mInsertionMarkerHorizontal); 166 dest.writeFloat(mInsertionMarkerTop); 167 dest.writeFloat(mInsertionMarkerBaseline); 168 dest.writeFloat(mInsertionMarkerBottom); 169 dest.writeParcelable(mCharacterRects, flags); 170 final float[] matrixArray = new float[9]; 171 mMatrix.getValues(matrixArray); 172 dest.writeFloatArray(matrixArray); 173 } 174 175 @Override 176 public int hashCode(){ 177 // TODO: Improve the hash function. 178 final float floatHash = mInsertionMarkerHorizontal + mInsertionMarkerTop 179 + mInsertionMarkerBaseline + mInsertionMarkerBottom; 180 int hash = floatHash > 0 ? (int) floatHash : (int)(-floatHash); 181 hash *= 31; 182 hash += mInsertionMarkerFlags; 183 hash *= 31; 184 hash += mSelectionStart + mSelectionEnd + mComposingTextStart; 185 hash *= 31; 186 hash += Objects.hashCode(mComposingText); 187 hash *= 31; 188 hash += Objects.hashCode(mCharacterRects); 189 hash *= 31; 190 hash += Objects.hashCode(mMatrix); 191 return hash; 192 } 193 194 /** 195 * Compares two float values. Returns {@code true} if {@code a} and {@code b} are 196 * {@link Float#NaN} at the same time. 197 */ 198 private static boolean areSameFloatImpl(final float a, final float b) { 199 if (Float.isNaN(a) && Float.isNaN(b)) { 200 return true; 201 } 202 return a == b; 203 } 204 205 @Override 206 public boolean equals(Object obj){ 207 if (obj == null) { 208 return false; 209 } 210 if (this == obj) { 211 return true; 212 } 213 if (!(obj instanceof CursorAnchorInfo)) { 214 return false; 215 } 216 final CursorAnchorInfo that = (CursorAnchorInfo) obj; 217 if (hashCode() != that.hashCode()) { 218 return false; 219 } 220 if (mSelectionStart != that.mSelectionStart || mSelectionEnd != that.mSelectionEnd) { 221 return false; 222 } 223 if (mComposingTextStart != that.mComposingTextStart 224 || !Objects.equals(mComposingText, that.mComposingText)) { 225 return false; 226 } 227 if (mInsertionMarkerFlags != that.mInsertionMarkerFlags 228 || !areSameFloatImpl(mInsertionMarkerHorizontal, that.mInsertionMarkerHorizontal) 229 || !areSameFloatImpl(mInsertionMarkerTop, that.mInsertionMarkerTop) 230 || !areSameFloatImpl(mInsertionMarkerBaseline, that.mInsertionMarkerBaseline) 231 || !areSameFloatImpl(mInsertionMarkerBottom, that.mInsertionMarkerBottom)) { 232 return false; 233 } 234 if (!Objects.equals(mCharacterRects, that.mCharacterRects)) { 235 return false; 236 } 237 if (!Objects.equals(mMatrix, that.mMatrix)) { 238 return false; 239 } 240 return true; 241 } 242 243 @Override 244 public String toString() { 245 return "SelectionInfo{mSelection=" + mSelectionStart + "," + mSelectionEnd 246 + " mComposingTextStart=" + mComposingTextStart 247 + " mComposingText=" + Objects.toString(mComposingText) 248 + " mInsertionMarkerFlags=" + mInsertionMarkerFlags 249 + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal 250 + " mInsertionMarkerTop=" + mInsertionMarkerTop 251 + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline 252 + " mInsertionMarkerBottom=" + mInsertionMarkerBottom 253 + " mCharacterRects=" + Objects.toString(mCharacterRects) 254 + " mMatrix=" + Objects.toString(mMatrix) 255 + "}"; 256 } 257 258 /** 259 * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe. 260 */ 261 public static final class Builder { 262 /** 263 * Sets the text range of the selection. Calling this can be skipped if there is no 264 * selection. 265 */ 266 public Builder setSelectionRange(final int newStart, final int newEnd) { 267 mSelectionStart = newStart; 268 mSelectionEnd = newEnd; 269 return this; 270 } 271 private int mSelectionStart = -1; 272 private int mSelectionEnd = -1; 273 274 /** 275 * Sets the text range of the composing text. Calling this can be skipped if there is 276 * no composing text. 277 * @param composingTextStart index where the composing text starts. 278 * @param composingText the entire composing text. 279 */ 280 public Builder setComposingText(final int composingTextStart, 281 final CharSequence composingText) { 282 mComposingTextStart = composingTextStart; 283 if (composingText == null) { 284 mComposingText = null; 285 } else { 286 // Make a snapshot of the given char sequence. 287 mComposingText = new SpannedString(composingText); 288 } 289 return this; 290 } 291 private int mComposingTextStart = -1; 292 private CharSequence mComposingText = null; 293 294 /** 295 * @removed 296 */ 297 public Builder setInsertionMarkerLocation(final float horizontalPosition, 298 final float lineTop, final float lineBaseline, final float lineBottom, 299 final boolean clipped){ 300 mInsertionMarkerHorizontal = horizontalPosition; 301 mInsertionMarkerTop = lineTop; 302 mInsertionMarkerBaseline = lineBaseline; 303 mInsertionMarkerBottom = lineBottom; 304 mInsertionMarkerFlags = clipped ? FLAG_HAS_INVISIBLE_REGION : 0; 305 return this; 306 } 307 308 /** 309 * Sets the location of the text insertion point (zero width cursor) as a rectangle in 310 * local coordinates. Calling this can be skipped when there is no text insertion point; 311 * however if there is an insertion point, editors must call this method. 312 * @param horizontalPosition horizontal position of the insertion marker, in the local 313 * coordinates that will be transformed with the transformation matrix when rendered on the 314 * screen. This should be calculated or compatible with 315 * {@link Layout#getPrimaryHorizontal(int)}. 316 * @param lineTop vertical position of the insertion marker, in the local coordinates that 317 * will be transformed with the transformation matrix when rendered on the screen. This 318 * should be calculated or compatible with {@link Layout#getLineTop(int)}. 319 * @param lineBaseline vertical position of the insertion marker, in the local coordinates 320 * that will be transformed with the transformation matrix when rendered on the screen. This 321 * should be calculated or compatible with {@link Layout#getLineBaseline(int)}. 322 * @param lineBottom vertical position of the insertion marker, in the local coordinates 323 * that will be transformed with the transformation matrix when rendered on the screen. This 324 * should be calculated or compatible with {@link Layout#getLineBottom(int)}. 325 * @param flags flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for 326 * example. 327 */ 328 public Builder setInsertionMarkerLocation(final float horizontalPosition, 329 final float lineTop, final float lineBaseline, final float lineBottom, 330 final int flags){ 331 mInsertionMarkerHorizontal = horizontalPosition; 332 mInsertionMarkerTop = lineTop; 333 mInsertionMarkerBaseline = lineBaseline; 334 mInsertionMarkerBottom = lineBottom; 335 mInsertionMarkerFlags = flags; 336 return this; 337 } 338 private float mInsertionMarkerHorizontal = Float.NaN; 339 private float mInsertionMarkerTop = Float.NaN; 340 private float mInsertionMarkerBaseline = Float.NaN; 341 private float mInsertionMarkerBottom = Float.NaN; 342 private int mInsertionMarkerFlags = 0; 343 344 /** 345 * Adds the bounding box of the character specified with the index. 346 * 347 * @param index index of the character in Java chars units. Must be specified in 348 * ascending order across successive calls. 349 * @param leadingEdgeX x coordinate of the leading edge of the character in local 350 * coordinates, that is, left edge for LTR text and right edge for RTL text. 351 * @param leadingEdgeY y coordinate of the leading edge of the character in local 352 * coordinates. 353 * @param trailingEdgeX x coordinate of the trailing edge of the character in local 354 * coordinates, that is, right edge for LTR text and left edge for RTL text. 355 * @param trailingEdgeY y coordinate of the trailing edge of the character in local 356 * coordinates. 357 * @param flags flags for this character rect. See {@link #FLAG_HAS_VISIBLE_REGION} for 358 * example. 359 * @throws IllegalArgumentException If the index is a negative value, or not greater than 360 * all of the previously called indices. 361 */ 362 public Builder addCharacterRect(final int index, final float leadingEdgeX, 363 final float leadingEdgeY, final float trailingEdgeX, final float trailingEdgeY, 364 final int flags) { 365 if (index < 0) { 366 throw new IllegalArgumentException("index must not be a negative integer."); 367 } 368 if (mCharacterRectBuilder == null) { 369 mCharacterRectBuilder = new SparseRectFArrayBuilder(); 370 } 371 mCharacterRectBuilder.append(index, leadingEdgeX, leadingEdgeY, trailingEdgeX, 372 trailingEdgeY, flags); 373 return this; 374 } 375 private SparseRectFArrayBuilder mCharacterRectBuilder = null; 376 377 /** 378 * Sets the matrix that transforms local coordinates into screen coordinates. 379 * @param matrix transformation matrix from local coordinates into screen coordinates. null 380 * is interpreted as an identity matrix. 381 */ 382 public Builder setMatrix(final Matrix matrix) { 383 mMatrix.set(matrix != null ? matrix : Matrix.IDENTITY_MATRIX); 384 mMatrixInitialized = true; 385 return this; 386 } 387 private final Matrix mMatrix = new Matrix(Matrix.IDENTITY_MATRIX); 388 private boolean mMatrixInitialized = false; 389 390 /** 391 * @return {@link CursorAnchorInfo} using parameters in this {@link Builder}. 392 * @throws IllegalArgumentException if one or more positional parameters are specified but 393 * the coordinate transformation matrix is not provided via {@link #setMatrix(Matrix)}. 394 */ 395 public CursorAnchorInfo build() { 396 if (!mMatrixInitialized) { 397 // Coordinate transformation matrix is mandatory when positional parameters are 398 // specified. 399 if ((mCharacterRectBuilder != null && !mCharacterRectBuilder.isEmpty()) || 400 !Float.isNaN(mInsertionMarkerHorizontal) || 401 !Float.isNaN(mInsertionMarkerTop) || 402 !Float.isNaN(mInsertionMarkerBaseline) || 403 !Float.isNaN(mInsertionMarkerBottom)) { 404 throw new IllegalArgumentException("Coordinate transformation matrix is " + 405 "required when positional parameters are specified."); 406 } 407 } 408 return new CursorAnchorInfo(this); 409 } 410 411 /** 412 * Resets the internal state so that this instance can be reused to build another 413 * instance of {@link CursorAnchorInfo}. 414 */ 415 public void reset() { 416 mSelectionStart = -1; 417 mSelectionEnd = -1; 418 mComposingTextStart = -1; 419 mComposingText = null; 420 mInsertionMarkerFlags = 0; 421 mInsertionMarkerHorizontal = Float.NaN; 422 mInsertionMarkerTop = Float.NaN; 423 mInsertionMarkerBaseline = Float.NaN; 424 mInsertionMarkerBottom = Float.NaN; 425 mMatrix.set(Matrix.IDENTITY_MATRIX); 426 mMatrixInitialized = false; 427 if (mCharacterRectBuilder != null) { 428 mCharacterRectBuilder.reset(); 429 } 430 } 431 } 432 433 private CursorAnchorInfo(final Builder builder) { 434 mSelectionStart = builder.mSelectionStart; 435 mSelectionEnd = builder.mSelectionEnd; 436 mComposingTextStart = builder.mComposingTextStart; 437 mComposingText = builder.mComposingText; 438 mInsertionMarkerFlags = builder.mInsertionMarkerFlags; 439 mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal; 440 mInsertionMarkerTop = builder.mInsertionMarkerTop; 441 mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline; 442 mInsertionMarkerBottom = builder.mInsertionMarkerBottom; 443 mCharacterRects = builder.mCharacterRectBuilder != null ? 444 builder.mCharacterRectBuilder.build() : null; 445 mMatrix = new Matrix(builder.mMatrix); 446 } 447 448 /** 449 * Returns the index where the selection starts. 450 * @return {@code -1} if there is no selection. 451 */ 452 public int getSelectionStart() { 453 return mSelectionStart; 454 } 455 456 /** 457 * Returns the index where the selection ends. 458 * @return {@code -1} if there is no selection. 459 */ 460 public int getSelectionEnd() { 461 return mSelectionEnd; 462 } 463 464 /** 465 * Returns the index where the composing text starts. 466 * @return {@code -1} if there is no composing text. 467 */ 468 public int getComposingTextStart() { 469 return mComposingTextStart; 470 } 471 472 /** 473 * Returns the entire composing text. 474 * @return {@code null} if there is no composition. 475 */ 476 public CharSequence getComposingText() { 477 return mComposingText; 478 } 479 480 /** 481 * Returns the flag of the insertion marker. 482 * @return the flag of the insertion marker. {@code 0} if no flag is specified. 483 */ 484 public int getInsertionMarkerFlags() { 485 return mInsertionMarkerFlags; 486 } 487 488 /** 489 * Returns the visibility of the insertion marker. 490 * @return {@code true} if the insertion marker is partially or entirely clipped. 491 * @removed 492 */ 493 public boolean isInsertionMarkerClipped() { 494 return (mInsertionMarkerFlags & FLAG_HAS_VISIBLE_REGION) != 0; 495 } 496 497 /** 498 * Returns the horizontal start of the insertion marker, in the local coordinates that will 499 * be transformed with {@link #getMatrix()} when rendered on the screen. 500 * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}. 501 * Pay special care to RTL/LTR handling. 502 * {@code java.lang.Float.NaN} if not specified. 503 * @see Layout#getPrimaryHorizontal(int) 504 */ 505 public float getInsertionMarkerHorizontal() { 506 return mInsertionMarkerHorizontal; 507 } 508 509 /** 510 * Returns the vertical top position of the insertion marker, in the local coordinates that 511 * will be transformed with {@link #getMatrix()} when rendered on the screen. 512 * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}. 513 * {@code java.lang.Float.NaN} if not specified. 514 */ 515 public float getInsertionMarkerTop() { 516 return mInsertionMarkerTop; 517 } 518 519 /** 520 * Returns the vertical baseline position of the insertion marker, in the local coordinates 521 * that will be transformed with {@link #getMatrix()} when rendered on the screen. 522 * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}. 523 * {@code java.lang.Float.NaN} if not specified. 524 */ 525 public float getInsertionMarkerBaseline() { 526 return mInsertionMarkerBaseline; 527 } 528 529 /** 530 * Returns the vertical bottom position of the insertion marker, in the local coordinates 531 * that will be transformed with {@link #getMatrix()} when rendered on the screen. 532 * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}. 533 * {@code java.lang.Float.NaN} if not specified. 534 */ 535 public float getInsertionMarkerBottom() { 536 return mInsertionMarkerBottom; 537 } 538 539 /** 540 * Returns a new instance of {@link RectF} that indicates the location of the character 541 * specified with the index. 542 * <p> 543 * Note that coordinates are not necessarily contiguous or even monotonous, especially when 544 * RTL text and LTR text are mixed. 545 * </p> 546 * @param index index of the character in a Java chars. 547 * @return a new instance of {@link RectF} that represents the location of the character in 548 * local coordinates. null if the character is invisible or the application did not provide 549 * the location. Note that the {@code left} field can be greater than the {@code right} field 550 * if the character is in RTL text. Returns {@code null} if no location information is 551 * available. 552 */ 553 // TODO: Prepare a document about the expected behavior for surrogate pairs, combining 554 // characters, and non-graphical chars. 555 public RectF getCharacterRect(final int index) { 556 if (mCharacterRects == null) { 557 return null; 558 } 559 return mCharacterRects.get(index); 560 } 561 562 /** 563 * Returns the flags associated with the character rect specified with the index. 564 * @param index index of the character in a Java chars. 565 * @return {@code 0} if no flag is specified. 566 */ 567 // TODO: Prepare a document about the expected behavior for surrogate pairs, combining 568 // characters, and non-graphical chars. 569 public int getCharacterRectFlags(final int index) { 570 if (mCharacterRects == null) { 571 return 0; 572 } 573 return mCharacterRects.getFlags(index, 0); 574 } 575 576 /** 577 * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation 578 * matrix that is to be applied other positional data in this class. 579 * @return a new instance (copy) of the transformation matrix. 580 */ 581 public Matrix getMatrix() { 582 return new Matrix(mMatrix); 583 } 584 585 /** 586 * Used to make this class parcelable. 587 */ 588 public static final Parcelable.Creator<CursorAnchorInfo> CREATOR 589 = new Parcelable.Creator<CursorAnchorInfo>() { 590 @Override 591 public CursorAnchorInfo createFromParcel(Parcel source) { 592 return new CursorAnchorInfo(source); 593 } 594 595 @Override 596 public CursorAnchorInfo[] newArray(int size) { 597 return new CursorAnchorInfo[size]; 598 } 599 }; 600 601 @Override 602 public int describeContents() { 603 return 0; 604 } 605} 606