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