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