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