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