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