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