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