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