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