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