1 /* 2 * Copyright (C) 2018 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 26 import dalvik.annotation.optimization.CriticalNative; 27 import dalvik.annotation.optimization.FastNative; 28 29 import libcore.util.NativeAllocationRegistry; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 34 /** 35 * Provides automatic line breaking for a <em>single</em> paragraph. 36 * 37 * <p> 38 * <pre> 39 * <code> 40 * Paint paint = new Paint(); 41 * Paint bigPaint = new Paint(); 42 * bigPaint.setTextSize(paint.getTextSize() * 2.0); 43 * String text = "Hello, Android."; 44 * 45 * // Prepare the measured text 46 * MeasuredText mt = new MeasuredText.Builder(text.toCharArray()) 47 * .appendStyleRun(paint, 7, false) // Use paint for "Hello, " 48 * .appednStyleRun(bigPaint, 8, false) // Use bigPaint for "Hello, " 49 * .build(); 50 * 51 * LineBreaker lb = new LineBreaker.Builder() 52 * // Use simple line breaker 53 * .setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE) 54 * // Do not add hyphenation. 55 * .setHyphenationFrequency(LineBreaker.HYPHENATION_FREQUENCY_NONE) 56 * // Build the LineBreaker 57 * .build(); 58 * 59 * ParagraphConstraints c = new ParagraphConstraints(); 60 * c.setWidth(240); // Set the line wieth as 1024px 61 * 62 * // Do the line breaking 63 * Result r = lb.computeLineBreaks(mt, c, 0); 64 * 65 * // Compute the total height of the text. 66 * float totalHeight = 0; 67 * for (int i = 0; i < r.getLineCount(); ++i) { // iterate over the lines 68 * totalHeight += r.getLineDescent(i) - r.getLineAscent(i); 69 * } 70 * 71 * // Draw text to the canvas 72 * Bitmap bmp = Bitmap.createBitmap(240, totalHeight, Bitmap.Config.ARGB_8888); 73 * Canvas c = new Canvas(bmp); 74 * float yOffset = 0f; 75 * int prevOffset = 0; 76 * for (int i = 0; i < r.getLineCount(); ++i) { // iterate over the lines 77 * int nextOffset = r.getLineBreakOffset(i); 78 * c.drawText(text, prevOffset, nextOffset, 0f, yOffset, paint); 79 * 80 * prevOffset = nextOffset; 81 * yOffset += r.getLineDescent(i) - r.getLineAscent(i); 82 * } 83 * </code> 84 * </pre> 85 * </p> 86 */ 87 public class LineBreaker { 88 /** @hide */ 89 @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { 90 BREAK_STRATEGY_SIMPLE, 91 BREAK_STRATEGY_HIGH_QUALITY, 92 BREAK_STRATEGY_BALANCED 93 }) 94 @Retention(RetentionPolicy.SOURCE) 95 public @interface BreakStrategy {} 96 97 /** 98 * Value for break strategy indicating simple line breaking. 99 * 100 * The line breaker puts words to the line as much as possible and breaks line if no more words 101 * can fit into the same line. Automatic hyphens are only added when a line has a single word 102 * and that word is longer than line width. This is the fastest break strategy and ideal for 103 * editor. 104 */ 105 public static final int BREAK_STRATEGY_SIMPLE = 0; 106 107 /** 108 * Value for break strategy indicating high quality line breaking. 109 * 110 * With this option line breaker does whole-paragraph optimization for more readable text, and 111 * also applies automatic hyphenation when required. 112 */ 113 public static final int BREAK_STRATEGY_HIGH_QUALITY = 1; 114 115 /** 116 * Value for break strategy indicating balanced line breaking. 117 * 118 * The line breaker does whole-paragraph optimization for making all lines similar length, and 119 * also applies automatic hyphenation when required. This break strategy is good for small 120 * screen devices such as watch screens. 121 */ 122 public static final int BREAK_STRATEGY_BALANCED = 2; 123 124 /** @hide */ 125 @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = { 126 HYPHENATION_FREQUENCY_NORMAL, 127 HYPHENATION_FREQUENCY_FULL, 128 HYPHENATION_FREQUENCY_NONE 129 }) 130 @Retention(RetentionPolicy.SOURCE) 131 public @interface HyphenationFrequency {} 132 133 /** 134 * Value for hyphenation frequency indicating no automatic hyphenation. 135 * 136 * Using this option disables auto hyphenation which results in better text layout performance. 137 * A word may be broken without hyphens when a line has a single word and that word is longer 138 * than line width. Soft hyphens are ignored and will not be used as suggestions for potential 139 * line breaks. 140 */ 141 public static final int HYPHENATION_FREQUENCY_NONE = 0; 142 143 /** 144 * Value for hyphenation frequency indicating a light amount of automatic hyphenation. 145 * 146 * This hyphenation frequency is useful for informal cases, such as short sentences or chat 147 * messages. 148 */ 149 public static final int HYPHENATION_FREQUENCY_NORMAL = 1; 150 151 /** 152 * Value for hyphenation frequency indicating the full amount of automatic hyphenation. 153 * 154 * This hyphenation frequency is useful for running text and where it's important to put the 155 * maximum amount of text in a screen with limited space. 156 */ 157 public static final int HYPHENATION_FREQUENCY_FULL = 2; 158 159 /** @hide */ 160 @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = { 161 JUSTIFICATION_MODE_NONE, 162 JUSTIFICATION_MODE_INTER_WORD 163 }) 164 @Retention(RetentionPolicy.SOURCE) 165 public @interface JustificationMode {} 166 167 /** 168 * Value for justification mode indicating no justification. 169 */ 170 public static final int JUSTIFICATION_MODE_NONE = 0; 171 172 /** 173 * Value for justification mode indicating the text is justified by stretching word spacing. 174 */ 175 public static final int JUSTIFICATION_MODE_INTER_WORD = 1; 176 177 /** 178 * Helper class for creating a {@link LineBreaker}. 179 */ 180 public static final class Builder { 181 private @BreakStrategy int mBreakStrategy = BREAK_STRATEGY_SIMPLE; 182 private @HyphenationFrequency int mHyphenationFrequency = HYPHENATION_FREQUENCY_NONE; 183 private @JustificationMode int mJustificationMode = JUSTIFICATION_MODE_NONE; 184 private @Nullable int[] mIndents = null; 185 186 /** 187 * Set break strategy. 188 * 189 * You can change the line breaking behavior by setting break strategy. The default value is 190 * {@link #BREAK_STRATEGY_SIMPLE}. 191 */ setBreakStrategy(@reakStrategy int breakStrategy)192 public @NonNull Builder setBreakStrategy(@BreakStrategy int breakStrategy) { 193 mBreakStrategy = breakStrategy; 194 return this; 195 } 196 197 /** 198 * Set hyphenation frequency. 199 * 200 * You can change the amount of automatic hyphenation used. The default value is 201 * {@link #HYPHENATION_FREQUENCY_NONE}. 202 */ setHyphenationFrequency( @yphenationFrequency int hyphenationFrequency)203 public @NonNull Builder setHyphenationFrequency( 204 @HyphenationFrequency int hyphenationFrequency) { 205 mHyphenationFrequency = hyphenationFrequency; 206 return this; 207 } 208 209 /** 210 * Set whether the text is justified. 211 * 212 * By setting {@link #JUSTIFICATION_MODE_INTER_WORD}, the line breaker will change the 213 * internal parameters for justification. 214 * The default value is {@link #JUSTIFICATION_MODE_NONE} 215 */ setJustificationMode(@ustificationMode int justificationMode)216 public @NonNull Builder setJustificationMode(@JustificationMode int justificationMode) { 217 mJustificationMode = justificationMode; 218 return this; 219 } 220 221 /** 222 * Set indents. 223 * 224 * The supplied array provides the total amount of indentation per line, in pixel. This 225 * amount is the sum of both left and right indentations. For lines past the last element in 226 * the array, the indentation amount of the last element is used. 227 */ setIndents(@ullable int[] indents)228 public @NonNull Builder setIndents(@Nullable int[] indents) { 229 mIndents = indents; 230 return this; 231 } 232 233 /** 234 * Build a new LineBreaker with given parameters. 235 * 236 * You can reuse the Builder instance even after calling this method. 237 */ build()238 public @NonNull LineBreaker build() { 239 return new LineBreaker(mBreakStrategy, mHyphenationFrequency, mJustificationMode, 240 mIndents); 241 } 242 } 243 244 /** 245 * Line breaking constraints for single paragraph. 246 */ 247 public static class ParagraphConstraints { 248 private @FloatRange(from = 0.0f) float mWidth = 0; 249 private @FloatRange(from = 0.0f) float mFirstWidth = 0; 250 private @IntRange(from = 0) int mFirstWidthLineCount = 0; 251 private @Nullable float[] mVariableTabStops = null; 252 private @FloatRange(from = 0) float mDefaultTabStop = 0; 253 ParagraphConstraints()254 public ParagraphConstraints() {} 255 256 /** 257 * Set width for this paragraph. 258 * 259 * @see #getWidth() 260 */ setWidth(@x @loatRangefrom = 0.0f) float width)261 public void setWidth(@Px @FloatRange(from = 0.0f) float width) { 262 mWidth = width; 263 } 264 265 /** 266 * Set indent for this paragraph. 267 * 268 * @param firstWidth the line width of the starting of the paragraph 269 * @param firstWidthLineCount the number of lines that applies the firstWidth 270 * @see #getFirstWidth() 271 * @see #getFirstWidthLineCount() 272 */ setIndent(@x @loatRangefrom = 0.0f) float firstWidth, @Px @IntRange(from = 0) int firstWidthLineCount)273 public void setIndent(@Px @FloatRange(from = 0.0f) float firstWidth, 274 @Px @IntRange(from = 0) int firstWidthLineCount) { 275 mFirstWidth = firstWidth; 276 mFirstWidthLineCount = firstWidthLineCount; 277 } 278 279 /** 280 * Set tab stops for this paragraph. 281 * 282 * @param tabStops the array of pixels of tap stopping position 283 * @param defaultTabStop pixels of the default tab stopping position 284 * @see #getTabStops() 285 * @see #getDefaultTabStop() 286 */ setTabStops(@ullable float[] tabStops, @Px @FloatRange(from = 0) float defaultTabStop)287 public void setTabStops(@Nullable float[] tabStops, 288 @Px @FloatRange(from = 0) float defaultTabStop) { 289 mVariableTabStops = tabStops; 290 mDefaultTabStop = defaultTabStop; 291 } 292 293 /** 294 * Return the width for this paragraph in pixels. 295 * 296 * @see #setWidth(float) 297 */ getWidth()298 public @Px @FloatRange(from = 0.0f) float getWidth() { 299 return mWidth; 300 } 301 302 /** 303 * Return the first line's width for this paragraph in pixel. 304 * 305 * @see #setIndent(float, int) 306 */ getFirstWidth()307 public @Px @FloatRange(from = 0.0f) float getFirstWidth() { 308 return mFirstWidth; 309 } 310 311 /** 312 * Return the number of lines to apply the first line's width. 313 * 314 * @see #setIndent(float, int) 315 */ getFirstWidthLineCount()316 public @Px @IntRange(from = 0) int getFirstWidthLineCount() { 317 return mFirstWidthLineCount; 318 } 319 320 /** 321 * Returns the array of tab stops in pixels. 322 * 323 * @see #setTabStops 324 */ getTabStops()325 public @Nullable float[] getTabStops() { 326 return mVariableTabStops; 327 } 328 329 /** 330 * Returns the default tab stops in pixels. 331 * 332 * @see #setTabStops 333 */ getDefaultTabStop()334 public @Px @FloatRange(from = 0) float getDefaultTabStop() { 335 return mDefaultTabStop; 336 } 337 } 338 339 /** 340 * Holds the result of the {@link LineBreaker#computeLineBreaks line breaking algorithm}. 341 * @see LineBreaker#computeLineBreaks 342 */ 343 public static class Result { 344 // Following two contstant must be synced with minikin's line breaker. 345 // TODO(nona): Remove these constatns by introducing native methods. 346 private static final int TAB_MASK = 0x20000000; 347 private static final int HYPHEN_MASK = 0xFF; 348 private static final int START_HYPHEN_MASK = 0x18; // 0b11000 349 private static final int END_HYPHEN_MASK = 0x7; // 0b00111 350 private static final int START_HYPHEN_BITS_SHIFT = 3; 351 352 private static final NativeAllocationRegistry sRegistry = 353 NativeAllocationRegistry.createMalloced( 354 Result.class.getClassLoader(), nGetReleaseResultFunc()); 355 private final long mPtr; 356 Result(long ptr)357 private Result(long ptr) { 358 mPtr = ptr; 359 sRegistry.registerNativeAllocation(this, mPtr); 360 } 361 362 /** 363 * Returns the number of lines in the paragraph. 364 * 365 * @return number of lines 366 */ getLineCount()367 public @IntRange(from = 0) int getLineCount() { 368 return nGetLineCount(mPtr); 369 } 370 371 /** 372 * Returns character offset of the break for a given line. 373 * 374 * @param lineIndex an index of the line. 375 * @return the break offset. 376 */ getLineBreakOffset(@ntRangefrom = 0) int lineIndex)377 public @IntRange(from = 0) int getLineBreakOffset(@IntRange(from = 0) int lineIndex) { 378 return nGetLineBreakOffset(mPtr, lineIndex); 379 } 380 381 /** 382 * Returns width of a given line in pixels. 383 * 384 * @param lineIndex an index of the line. 385 * @return width of the line in pixels 386 */ getLineWidth(@ntRangefrom = 0) int lineIndex)387 public @Px float getLineWidth(@IntRange(from = 0) int lineIndex) { 388 return nGetLineWidth(mPtr, lineIndex); 389 } 390 391 /** 392 * Returns font ascent of the line in pixels. 393 * 394 * @param lineIndex an index of the line. 395 * @return an entier font ascent of the line in pixels. 396 */ getLineAscent(@ntRangefrom = 0) int lineIndex)397 public @Px float getLineAscent(@IntRange(from = 0) int lineIndex) { 398 return nGetLineAscent(mPtr, lineIndex); 399 } 400 401 /** 402 * Returns font descent of the line in pixels. 403 * 404 * @param lineIndex an index of the line. 405 * @return an entier font descent of the line in pixels. 406 */ getLineDescent(@ntRangefrom = 0) int lineIndex)407 public @Px float getLineDescent(@IntRange(from = 0) int lineIndex) { 408 return nGetLineDescent(mPtr, lineIndex); 409 } 410 411 /** 412 * Returns true if the line has a TAB character. 413 * 414 * @param lineIndex an index of the line. 415 * @return true if the line has a TAB character 416 */ hasLineTab(int lineIndex)417 public boolean hasLineTab(int lineIndex) { 418 return (nGetLineFlag(mPtr, lineIndex) & TAB_MASK) != 0; 419 } 420 421 /** 422 * Returns a start hyphen edit for the line. 423 * 424 * @param lineIndex an index of the line. 425 * @return a start hyphen edit for the line. 426 * 427 * @see android.graphics.Paint#setStartHyphenEdit 428 * @see android.graphics.Paint#getStartHyphenEdit 429 */ getStartLineHyphenEdit(int lineIndex)430 public int getStartLineHyphenEdit(int lineIndex) { 431 return (nGetLineFlag(mPtr, lineIndex) & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT; 432 } 433 434 /** 435 * Returns an end hyphen edit for the line. 436 * 437 * @param lineIndex an index of the line. 438 * @return an end hyphen edit for the line. 439 * 440 * @see android.graphics.Paint#setEndHyphenEdit 441 * @see android.graphics.Paint#getEndHyphenEdit 442 */ getEndLineHyphenEdit(int lineIndex)443 public int getEndLineHyphenEdit(int lineIndex) { 444 return nGetLineFlag(mPtr, lineIndex) & END_HYPHEN_MASK; 445 } 446 } 447 448 private static final NativeAllocationRegistry sRegistry = 449 NativeAllocationRegistry.createMalloced( 450 LineBreaker.class.getClassLoader(), nGetReleaseFunc()); 451 452 private final long mNativePtr; 453 454 /** 455 * Use Builder instead. 456 */ LineBreaker(@reakStrategy int breakStrategy, @HyphenationFrequency int hyphenationFrequency, @JustificationMode int justify, @Nullable int[] indents)457 private LineBreaker(@BreakStrategy int breakStrategy, 458 @HyphenationFrequency int hyphenationFrequency, @JustificationMode int justify, 459 @Nullable int[] indents) { 460 mNativePtr = nInit(breakStrategy, hyphenationFrequency, 461 justify == JUSTIFICATION_MODE_INTER_WORD, indents); 462 sRegistry.registerNativeAllocation(this, mNativePtr); 463 } 464 465 /** 466 * Break paragraph into lines. 467 * 468 * The result is filled to out param. 469 * 470 * @param measuredPara a result of the text measurement 471 * @param constraints for a single paragraph 472 * @param lineNumber a line number of this paragraph 473 */ computeLineBreaks( @onNull MeasuredText measuredPara, @NonNull ParagraphConstraints constraints, @IntRange(from = 0) int lineNumber)474 public @NonNull Result computeLineBreaks( 475 @NonNull MeasuredText measuredPara, 476 @NonNull ParagraphConstraints constraints, 477 @IntRange(from = 0) int lineNumber) { 478 return new Result(nComputeLineBreaks( 479 mNativePtr, 480 481 // Inputs 482 measuredPara.getChars(), 483 measuredPara.getNativePtr(), 484 measuredPara.getChars().length, 485 constraints.mFirstWidth, 486 constraints.mFirstWidthLineCount, 487 constraints.mWidth, 488 constraints.mVariableTabStops, 489 constraints.mDefaultTabStop, 490 lineNumber)); 491 } 492 493 @FastNative nInit(@reakStrategy int breakStrategy, @HyphenationFrequency int hyphenationFrequency, boolean isJustified, @Nullable int[] indents)494 private static native long nInit(@BreakStrategy int breakStrategy, 495 @HyphenationFrequency int hyphenationFrequency, boolean isJustified, 496 @Nullable int[] indents); 497 498 @CriticalNative nGetReleaseFunc()499 private static native long nGetReleaseFunc(); 500 501 // populates LineBreaks and returns the number of breaks found 502 // 503 // the arrays inside the LineBreaks objects are passed in as well 504 // to reduce the number of JNI calls in the common case where the 505 // arrays do not have to be resized 506 // The individual character widths will be returned in charWidths. The length of 507 // charWidths must be at least the length of the text. nComputeLineBreaks( long nativePtr, @NonNull char[] text, long measuredTextPtr, @IntRange(from = 0) int length, @FloatRange(from = 0.0f) float firstWidth, @IntRange(from = 0) int firstWidthLineCount, @FloatRange(from = 0.0f) float restWidth, @Nullable float[] variableTabStops, float defaultTabStop, @IntRange(from = 0) int indentsOffset)508 private static native long nComputeLineBreaks( 509 /* non zero */ long nativePtr, 510 511 // Inputs 512 @NonNull char[] text, 513 /* Non Zero */ long measuredTextPtr, 514 @IntRange(from = 0) int length, 515 @FloatRange(from = 0.0f) float firstWidth, 516 @IntRange(from = 0) int firstWidthLineCount, 517 @FloatRange(from = 0.0f) float restWidth, 518 @Nullable float[] variableTabStops, 519 float defaultTabStop, 520 @IntRange(from = 0) int indentsOffset); 521 522 // Result accessors 523 @CriticalNative nGetLineCount(long ptr)524 private static native int nGetLineCount(long ptr); 525 @CriticalNative nGetLineBreakOffset(long ptr, int idx)526 private static native int nGetLineBreakOffset(long ptr, int idx); 527 @CriticalNative nGetLineWidth(long ptr, int idx)528 private static native float nGetLineWidth(long ptr, int idx); 529 @CriticalNative nGetLineAscent(long ptr, int idx)530 private static native float nGetLineAscent(long ptr, int idx); 531 @CriticalNative nGetLineDescent(long ptr, int idx)532 private static native float nGetLineDescent(long ptr, int idx); 533 @CriticalNative nGetLineFlag(long ptr, int idx)534 private static native int nGetLineFlag(long ptr, int idx); 535 @CriticalNative nGetReleaseResultFunc()536 private static native long nGetReleaseResultFunc(); 537 } 538