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