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 CursorAnchorInfo(final Parcel source)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 writeToParcel(Parcel dest, int flags)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 hashCode()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 */ areSameFloatImpl(final float a, final float b)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 equals(Object obj)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 toString()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 */ setSelectionRange(final int newStart, final int newEnd)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 */ setComposingText(final int composingTextStart, final CharSequence composingText)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 */ setInsertionMarkerLocation(final float horizontalPosition, final float lineTop, final float lineBaseline, final float lineBottom, final int flags)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 */ addCharacterBounds(final int index, final float left, final float top, final float right, final float bottom, final int flags)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 */ setMatrix(final Matrix matrix)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 */ build()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 */ reset()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 CursorAnchorInfo(final Builder builder)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 */ getSelectionStart()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 */ getSelectionEnd()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 */ getComposingTextStart()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 */ getComposingText()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 */ getInsertionMarkerFlags()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 */ getInsertionMarkerHorizontal()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 */ getInsertionMarkerTop()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 */ getInsertionMarkerBaseline()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 */ getInsertionMarkerBottom()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 */ getCharacterBounds(final int index)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 */ getCharacterBoundsFlags(final int index)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 */ getMatrix()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 describeContents()576 public int describeContents() { 577 return 0; 578 } 579 } 580