1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.graphics.text; 18 19 import android.annotation.FloatRange; 20 import android.annotation.IntDef; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.Px; 25 import android.graphics.Paint; 26 import android.graphics.Rect; 27 import android.util.Log; 28 29 import com.android.internal.util.Preconditions; 30 31 import dalvik.annotation.optimization.CriticalNative; 32 33 import libcore.util.NativeAllocationRegistry; 34 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 import java.util.Objects; 38 39 /** 40 * Result of text shaping of the single paragraph string. 41 * 42 * <p> 43 * <pre> 44 * <code> 45 * Paint paint = new Paint(); 46 * Paint bigPaint = new Paint(); 47 * bigPaint.setTextSize(paint.getTextSize() * 2.0); 48 * String text = "Hello, Android."; 49 * MeasuredText mt = new MeasuredText.Builder(text.toCharArray()) 50 * .appendStyleRun(paint, 7, false) // Use paint for "Hello, " 51 * .appendStyleRun(bigPaint, 8, false) // Use bigPaint for "Android." 52 * .build(); 53 * </code> 54 * </pre> 55 * </p> 56 */ 57 public class MeasuredText { 58 private static final String TAG = "MeasuredText"; 59 60 private final long mNativePtr; 61 private final boolean mComputeHyphenation; 62 private final boolean mComputeLayout; 63 @NonNull private final char[] mChars; 64 private final int mTop; 65 private final int mBottom; 66 67 // Use builder instead. MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation, boolean computeLayout, int top, int bottom)68 private MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation, 69 boolean computeLayout, int top, int bottom) { 70 mNativePtr = ptr; 71 mChars = chars; 72 mComputeHyphenation = computeHyphenation; 73 mComputeLayout = computeLayout; 74 mTop = top; 75 mBottom = bottom; 76 } 77 78 /** 79 * Returns the characters in the paragraph used to compute this MeasuredText instance. 80 * @hide 81 */ getChars()82 public @NonNull char[] getChars() { 83 return mChars; 84 } 85 86 /** 87 * Returns the width of a given range. 88 * 89 * @param start an inclusive start index of the range 90 * @param end an exclusive end index of the range 91 */ getWidth( @ntRangefrom = 0) int start, @IntRange(from = 0) int end)92 public @FloatRange(from = 0.0) @Px float getWidth( 93 @IntRange(from = 0) int start, @IntRange(from = 0) int end) { 94 Preconditions.checkArgument(0 <= start && start <= mChars.length, 95 "start(%d) must be 0 <= start <= %d", start, mChars.length); 96 Preconditions.checkArgument(0 <= end && end <= mChars.length, 97 "end(%d) must be 0 <= end <= %d", end, mChars.length); 98 Preconditions.checkArgument(start <= end, 99 "start(%d) is larger than end(%d)", start, end); 100 return nGetWidth(mNativePtr, start, end); 101 } 102 103 /** 104 * Returns a memory usage of the native object. 105 * 106 * @hide 107 */ getMemoryUsage()108 public int getMemoryUsage() { 109 return nGetMemoryUsage(mNativePtr); 110 } 111 112 /** 113 * Retrieves the boundary box of the given range 114 * 115 * @param start an inclusive start index of the range 116 * @param end an exclusive end index of the range 117 * @param rect an output parameter 118 */ getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect rect)119 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 120 @NonNull Rect rect) { 121 Preconditions.checkArgument(0 <= start && start <= mChars.length, 122 "start(%d) must be 0 <= start <= %d", start, mChars.length); 123 Preconditions.checkArgument(0 <= end && end <= mChars.length, 124 "end(%d) must be 0 <= end <= %d", end, mChars.length); 125 Preconditions.checkArgument(start <= end, 126 "start(%d) is larger than end(%d)", start, end); 127 Preconditions.checkNotNull(rect); 128 nGetBounds(mNativePtr, mChars, start, end, rect); 129 } 130 131 /** 132 * Retrieves the font metrics of the given range 133 * 134 * @param start an inclusive start index of the range 135 * @param end an exclusive end index of the range 136 * @param outMetrics an output metrics object 137 */ getFontMetricsInt(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Paint.FontMetricsInt outMetrics)138 public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 139 @NonNull Paint.FontMetricsInt outMetrics) { 140 Preconditions.checkArgument(0 <= start && start <= mChars.length, 141 "start(%d) must be 0 <= start <= %d", start, mChars.length); 142 Preconditions.checkArgument(0 <= end && end <= mChars.length, 143 "end(%d) must be 0 <= end <= %d", end, mChars.length); 144 Preconditions.checkArgument(start <= end, 145 "start(%d) is larger than end(%d)", start, end); 146 Objects.requireNonNull(outMetrics); 147 148 long packed = nGetExtent(mNativePtr, mChars, start, end); 149 outMetrics.ascent = (int) (packed >> 32); 150 outMetrics.descent = (int) (packed & 0xFFFFFFFF); 151 outMetrics.top = Math.min(outMetrics.ascent, mTop); 152 outMetrics.bottom = Math.max(outMetrics.descent, mBottom); 153 } 154 155 /** 156 * Returns the width of the character at the given offset. 157 * 158 * @param offset an offset of the character. 159 */ getCharWidthAt(@ntRangefrom = 0) int offset)160 public @FloatRange(from = 0.0f) @Px float getCharWidthAt(@IntRange(from = 0) int offset) { 161 Preconditions.checkArgument(0 <= offset && offset < mChars.length, 162 "offset(%d) is larger than text length %d" + offset, mChars.length); 163 return nGetCharWidthAt(mNativePtr, offset); 164 } 165 166 /** 167 * Returns a native pointer of the underlying native object. 168 * 169 * @hide 170 */ getNativePtr()171 public long getNativePtr() { 172 return mNativePtr; 173 } 174 175 @CriticalNative nGetWidth( long nativePtr, @IntRange(from = 0) int start, @IntRange(from = 0) int end)176 private static native float nGetWidth(/* Non Zero */ long nativePtr, 177 @IntRange(from = 0) int start, 178 @IntRange(from = 0) int end); 179 180 @CriticalNative nGetReleaseFunc()181 private static native /* Non Zero */ long nGetReleaseFunc(); 182 183 @CriticalNative nGetMemoryUsage( long nativePtr)184 private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr); 185 nGetBounds(long nativePtr, char[] buf, int start, int end, Rect rect)186 private static native void nGetBounds(long nativePtr, char[] buf, int start, int end, 187 Rect rect); 188 189 @CriticalNative nGetCharWidthAt(long nativePtr, int offset)190 private static native float nGetCharWidthAt(long nativePtr, int offset); 191 nGetExtent(long nativePtr, char[] buf, int start, int end)192 private static native long nGetExtent(long nativePtr, char[] buf, int start, int end); 193 194 /** 195 * Helper class for creating a {@link MeasuredText}. 196 * <p> 197 * <pre> 198 * <code> 199 * Paint paint = new Paint(); 200 * String text = "Hello, Android."; 201 * MeasuredText mt = new MeasuredText.Builder(text.toCharArray()) 202 * .appendStyleRun(paint, text.length, false) 203 * .build(); 204 * </code> 205 * </pre> 206 * </p> 207 * 208 * Note: The appendStyle and appendReplacementRun should be called to cover the text length. 209 */ 210 public static final class Builder { 211 private static final NativeAllocationRegistry sRegistry = 212 NativeAllocationRegistry.createMalloced( 213 MeasuredText.class.getClassLoader(), nGetReleaseFunc()); 214 215 private long mNativePtr; 216 217 private final @NonNull char[] mText; 218 private boolean mComputeHyphenation = false; 219 private boolean mComputeLayout = true; 220 private boolean mFastHyphenation = false; 221 private int mCurrentOffset = 0; 222 private @Nullable MeasuredText mHintMt = null; 223 private int mTop = 0; 224 private int mBottom = 0; 225 private Paint.FontMetricsInt mCachedMetrics = new Paint.FontMetricsInt(); 226 227 /** 228 * Construct a builder. 229 * 230 * The MeasuredText returned by build method will hold a reference of the text. Developer is 231 * not supposed to modify the text. 232 * 233 * @param text a text 234 */ Builder(@onNull char[] text)235 public Builder(@NonNull char[] text) { 236 Preconditions.checkNotNull(text); 237 mText = text; 238 mNativePtr = nInitBuilder(); 239 } 240 241 /** 242 * Construct a builder with existing MeasuredText. 243 * 244 * The MeasuredText returned by build method will hold a reference of the text. Developer is 245 * not supposed to modify the text. 246 * 247 * @param text a text 248 */ Builder(@onNull MeasuredText text)249 public Builder(@NonNull MeasuredText text) { 250 Preconditions.checkNotNull(text); 251 mText = text.mChars; 252 mNativePtr = nInitBuilder(); 253 if (!text.mComputeLayout) { 254 throw new IllegalArgumentException( 255 "The input MeasuredText must not be created with setComputeLayout(false)."); 256 } 257 mComputeHyphenation = text.mComputeHyphenation; 258 mComputeLayout = text.mComputeLayout; 259 mHintMt = text; 260 } 261 262 /** 263 * Apply styles to the given length. 264 * 265 * Keeps an internal offset which increases at every append. The initial value for this 266 * offset is zero. After the style is applied the internal offset is moved to {@code offset 267 * + length}, and next call will start from this new position. 268 * 269 * @param paint a paint 270 * @param length a length to be applied with a given paint, can not exceed the length of the 271 * text 272 * @param isRtl true if the text is in RTL context, otherwise false. 273 */ appendStyleRun(@onNull Paint paint, @IntRange(from = 0) int length, boolean isRtl)274 public @NonNull Builder appendStyleRun(@NonNull Paint paint, @IntRange(from = 0) int length, 275 boolean isRtl) { 276 return appendStyleRun(paint, null, length, isRtl); 277 } 278 279 /** 280 * Apply styles to the given length. 281 * 282 * Keeps an internal offset which increases at every append. The initial value for this 283 * offset is zero. After the style is applied the internal offset is moved to {@code offset 284 * + length}, and next call will start from this new position. 285 * 286 * @param paint a paint 287 * @param lineBreakConfig a line break configuration. 288 * @param length a length to be applied with a given paint, can not exceed the length of the 289 * text 290 * @param isRtl true if the text is in RTL context, otherwise false. 291 */ appendStyleRun(@onNull Paint paint, @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length, boolean isRtl)292 public @NonNull Builder appendStyleRun(@NonNull Paint paint, 293 @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length, 294 boolean isRtl) { 295 Preconditions.checkNotNull(paint); 296 Preconditions.checkArgument(length > 0, "length can not be negative"); 297 final int end = mCurrentOffset + length; 298 Preconditions.checkArgument(end <= mText.length, "Style exceeds the text length"); 299 int lbStyle = (lineBreakConfig != null) ? lineBreakConfig.getLineBreakStyle() : 300 LineBreakConfig.LINE_BREAK_STYLE_NONE; 301 int lbWordStyle = (lineBreakConfig != null) ? lineBreakConfig.getLineBreakWordStyle() : 302 LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE; 303 nAddStyleRun(mNativePtr, paint.getNativeInstance(), lbStyle, lbWordStyle, 304 mCurrentOffset, end, isRtl); 305 mCurrentOffset = end; 306 307 paint.getFontMetricsInt(mCachedMetrics); 308 mTop = Math.min(mTop, mCachedMetrics.top); 309 mBottom = Math.max(mBottom, mCachedMetrics.bottom); 310 return this; 311 } 312 313 /** 314 * Used to inform the text layout that the given length is replaced with the object of given 315 * width. 316 * 317 * Keeps an internal offset which increases at every append. The initial value for this 318 * offset is zero. After the style is applied the internal offset is moved to {@code offset 319 * + length}, and next call will start from this new position. 320 * 321 * Informs the layout engine that the given length should not be processed, instead the 322 * provided width should be used for calculating the width of that range. 323 * 324 * @param length a length to be replaced with the object, can not exceed the length of the 325 * text 326 * @param width a replacement width of the range 327 */ appendReplacementRun(@onNull Paint paint, @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width)328 public @NonNull Builder appendReplacementRun(@NonNull Paint paint, 329 @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width) { 330 Preconditions.checkArgument(length > 0, "length can not be negative"); 331 final int end = mCurrentOffset + length; 332 Preconditions.checkArgument(end <= mText.length, "Replacement exceeds the text length"); 333 nAddReplacementRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, width); 334 mCurrentOffset = end; 335 return this; 336 } 337 338 /** 339 * By passing true to this method, the build method will compute all possible hyphenation 340 * pieces as well. 341 * 342 * If you don't want to use automatic hyphenation, you can pass false to this method and 343 * save the computation time of hyphenation. The default value is false. 344 * 345 * Even if you pass false to this method, you can still enable automatic hyphenation of 346 * LineBreaker but line break computation becomes slower. 347 * 348 * @deprecated use setComputeHyphenation(int) instead. 349 * 350 * @param computeHyphenation true if you want to use automatic hyphenations. 351 */ setComputeHyphenation(boolean computeHyphenation)352 public @NonNull @Deprecated Builder setComputeHyphenation(boolean computeHyphenation) { 353 setComputeHyphenation( 354 computeHyphenation ? HYPHENATION_MODE_NORMAL : HYPHENATION_MODE_NONE); 355 return this; 356 } 357 358 /** @hide */ 359 @IntDef(prefix = { "HYPHENATION_MODE_" }, value = { 360 HYPHENATION_MODE_NONE, 361 HYPHENATION_MODE_NORMAL, 362 HYPHENATION_MODE_FAST 363 }) 364 @Retention(RetentionPolicy.SOURCE) 365 public @interface HyphenationMode {} 366 367 /** 368 * A value for hyphenation calculation mode. 369 * 370 * This value indicates that no hyphenation points are calculated. 371 */ 372 public static final int HYPHENATION_MODE_NONE = 0; 373 374 /** 375 * A value for hyphenation calculation mode. 376 * 377 * This value indicates that hyphenation points are calculated. 378 */ 379 public static final int HYPHENATION_MODE_NORMAL = 1; 380 381 /** 382 * A value for hyphenation calculation mode. 383 * 384 * This value indicates that hyphenation points are calculated with faster algorithm. This 385 * algorithm measures text width with ignoring the context of hyphen character shaping, e.g. 386 * kerning. 387 */ 388 public static final int HYPHENATION_MODE_FAST = 2; 389 390 /** 391 * By passing true to this method, the build method will calculate hyphenation break 392 * points faster with ignoring some typographic features, e.g. kerning. 393 * 394 * {@link #HYPHENATION_MODE_NONE} is by default. 395 * 396 * @param mode a hyphenation mode. 397 */ setComputeHyphenation(@yphenationMode int mode)398 public @NonNull Builder setComputeHyphenation(@HyphenationMode int mode) { 399 switch (mode) { 400 case HYPHENATION_MODE_NONE: 401 mComputeHyphenation = false; 402 mFastHyphenation = false; 403 break; 404 case HYPHENATION_MODE_NORMAL: 405 mComputeHyphenation = true; 406 mFastHyphenation = false; 407 break; 408 case HYPHENATION_MODE_FAST: 409 mComputeHyphenation = true; 410 mFastHyphenation = true; 411 break; 412 default: 413 Log.e(TAG, "Unknown hyphenation mode: " + mode); 414 mComputeHyphenation = false; 415 mFastHyphenation = false; 416 break; 417 } 418 return this; 419 } 420 421 /** 422 * By passing true to this method, the build method will compute all full layout 423 * information. 424 * 425 * If you don't use {@link MeasuredText#getBounds(int,int,android.graphics.Rect)}, you can 426 * pass false to this method and save the memory spaces. The default value is true. 427 * 428 * Even if you pass false to this method, you can still call getBounds but it becomes 429 * slower. 430 * 431 * @param computeLayout true if you want to retrieve full layout info, e.g. bbox. 432 */ setComputeLayout(boolean computeLayout)433 public @NonNull Builder setComputeLayout(boolean computeLayout) { 434 mComputeLayout = computeLayout; 435 return this; 436 } 437 438 /** 439 * Creates a MeasuredText. 440 * 441 * Once you called build() method, you can't reuse the Builder class again. 442 * @throws IllegalStateException if this Builder is reused. 443 * @throws IllegalStateException if the whole text is not covered by one or more runs (style 444 * or replacement) 445 */ build()446 public @NonNull MeasuredText build() { 447 ensureNativePtrNoReuse(); 448 if (mCurrentOffset != mText.length) { 449 throw new IllegalStateException("Style info has not been provided for all text."); 450 } 451 if (mHintMt != null && mHintMt.mComputeHyphenation != mComputeHyphenation) { 452 throw new IllegalArgumentException( 453 "The hyphenation configuration is different from given hint MeasuredText"); 454 } 455 try { 456 long hintPtr = (mHintMt == null) ? 0 : mHintMt.getNativePtr(); 457 long ptr = nBuildMeasuredText(mNativePtr, hintPtr, mText, mComputeHyphenation, 458 mComputeLayout, mFastHyphenation); 459 final MeasuredText res = new MeasuredText(ptr, mText, mComputeHyphenation, 460 mComputeLayout, mTop, mBottom); 461 sRegistry.registerNativeAllocation(res, ptr); 462 return res; 463 } finally { 464 nFreeBuilder(mNativePtr); 465 mNativePtr = 0; 466 } 467 } 468 469 /** 470 * Ensures {@link #mNativePtr} is not reused. 471 * 472 * <p/> This is a method by itself to help increase testability - eg. Robolectric might want 473 * to override the validation behavior in test environment. 474 */ ensureNativePtrNoReuse()475 private void ensureNativePtrNoReuse() { 476 if (mNativePtr == 0) { 477 throw new IllegalStateException("Builder can not be reused."); 478 } 479 } 480 nInitBuilder()481 private static native /* Non Zero */ long nInitBuilder(); 482 483 /** 484 * Apply style to make native measured text. 485 * 486 * @param nativeBuilderPtr The native MeasuredParagraph builder pointer. 487 * @param paintPtr The native paint pointer to be applied. 488 * @param lineBreakStyle The line break style(lb) of the text. 489 * @param lineBreakWordStyle The line break word style(lw) of the text. 490 * @param start The start offset in the copied buffer. 491 * @param end The end offset in the copied buffer. 492 * @param isRtl True if the text is RTL. 493 */ nAddStyleRun( long nativeBuilderPtr, long paintPtr, int lineBreakStyle, int lineBreakWordStyle, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl)494 private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr, 495 /* Non Zero */ long paintPtr, 496 int lineBreakStyle, 497 int lineBreakWordStyle, 498 @IntRange(from = 0) int start, 499 @IntRange(from = 0) int end, 500 boolean isRtl); 501 /** 502 * Apply ReplacementRun to make native measured text. 503 * 504 * @param nativeBuilderPtr The native MeasuredParagraph builder pointer. 505 * @param paintPtr The native paint pointer to be applied. 506 * @param start The start offset in the copied buffer. 507 * @param end The end offset in the copied buffer. 508 * @param width The width of the replacement. 509 */ nAddReplacementRun( long nativeBuilderPtr, long paintPtr, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @FloatRange(from = 0) float width)510 private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr, 511 /* Non Zero */ long paintPtr, 512 @IntRange(from = 0) int start, 513 @IntRange(from = 0) int end, 514 @FloatRange(from = 0) float width); 515 nBuildMeasuredText( long nativeBuilderPtr, long hintMtPtr, @NonNull char[] text, boolean computeHyphenation, boolean computeLayout, boolean fastHyphenationMode)516 private static native long nBuildMeasuredText( 517 /* Non Zero */ long nativeBuilderPtr, 518 long hintMtPtr, 519 @NonNull char[] text, 520 boolean computeHyphenation, 521 boolean computeLayout, 522 boolean fastHyphenationMode); 523 nFreeBuilder( long nativeBuilderPtr)524 private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr); 525 } 526 } 527