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