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