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.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.Px;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import dalvik.annotation.optimization.CriticalNative;
30 
31 import libcore.util.NativeAllocationRegistry;
32 
33 /**
34  * Result of text shaping of the single paragraph string.
35  *
36  * <p>
37  * <pre>
38  * <code>
39  * Paint paint = new Paint();
40  * Paint bigPaint = new Paint();
41  * bigPaint.setTextSize(paint.getTextSize() * 2.0);
42  * String text = "Hello, Android.";
43  * MeasuredText mt = new MeasuredText.Builder(text.toCharArray())
44  *      .appendStyleRun(paint, 7, false)  // Use paint for "Hello, "
45  *      .appendStyleRun(bigPaint, 8, false)  // Use bigPaint for "Android."
46  *      .build();
47  * </code>
48  * </pre>
49  * </p>
50  */
51 public class MeasuredText {
52     private long mNativePtr;
53     private boolean mComputeHyphenation;
54     private boolean mComputeLayout;
55     private @NonNull char[] mChars;
56 
57     // Use builder instead.
MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation, boolean computeLayout)58     private MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation,
59             boolean computeLayout) {
60         mNativePtr = ptr;
61         mChars = chars;
62         mComputeHyphenation = computeHyphenation;
63         mComputeLayout = computeLayout;
64     }
65 
66     /**
67      * Returns the characters in the paragraph used to compute this MeasuredText instance.
68      * @hide
69      */
getChars()70     public @NonNull char[] getChars() {
71         return mChars;
72     }
73 
74     /**
75      * Returns the width of a given range.
76      *
77      * @param start an inclusive start index of the range
78      * @param end an exclusive end index of the range
79      */
getWidth( @ntRangefrom = 0) int start, @IntRange(from = 0) int end)80     public @FloatRange(from = 0.0) @Px float getWidth(
81             @IntRange(from = 0) int start, @IntRange(from = 0) int end) {
82         Preconditions.checkArgument(0 <= start && start <= mChars.length,
83                 "start(" + start + ") must be 0 <= start <= " + mChars.length);
84         Preconditions.checkArgument(0 <= end && end <= mChars.length,
85                 "end(" + end + ") must be 0 <= end <= " + mChars.length);
86         Preconditions.checkArgument(start <= end,
87                 "start(" + start + ") is larger than end(" + end + ")");
88         return nGetWidth(mNativePtr, start, end);
89     }
90 
91     /**
92      * Returns a memory usage of the native object.
93      *
94      * @hide
95      */
getMemoryUsage()96     public int getMemoryUsage() {
97         return nGetMemoryUsage(mNativePtr);
98     }
99 
100     /**
101      * Retrieves the boundary box of the given range
102      *
103      * @param start an inclusive start index of the range
104      * @param end an exclusive end index of the range
105      * @param rect an output parameter
106      */
getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect rect)107     public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
108             @NonNull Rect rect) {
109         Preconditions.checkArgument(0 <= start && start <= mChars.length,
110                 "start(" + start + ") must be 0 <= start <= " + mChars.length);
111         Preconditions.checkArgument(0 <= end && end <= mChars.length,
112                 "end(" + end + ") must be 0 <= end <= " + mChars.length);
113         Preconditions.checkArgument(start <= end,
114                 "start(" + start + ") is larger than end(" + end + ")");
115         Preconditions.checkNotNull(rect);
116         nGetBounds(mNativePtr, mChars, start, end, rect);
117     }
118 
119     /**
120      * Returns the width of the character at the given offset.
121      *
122      * @param offset an offset of the character.
123      */
getCharWidthAt(@ntRangefrom = 0) int offset)124     public @FloatRange(from = 0.0f) @Px float getCharWidthAt(@IntRange(from = 0) int offset) {
125         Preconditions.checkArgument(0 <= offset && offset < mChars.length,
126                 "offset(" + offset + ") is larger than text length: " + mChars.length);
127         return nGetCharWidthAt(mNativePtr, offset);
128     }
129 
130     /**
131      * Returns a native pointer of the underlying native object.
132      *
133      * @hide
134      */
getNativePtr()135     public long getNativePtr() {
136         return mNativePtr;
137     }
138 
139     @CriticalNative
nGetWidth( long nativePtr, @IntRange(from = 0) int start, @IntRange(from = 0) int end)140     private static native float nGetWidth(/* Non Zero */ long nativePtr,
141                                          @IntRange(from = 0) int start,
142                                          @IntRange(from = 0) int end);
143 
144     @CriticalNative
nGetReleaseFunc()145     private static native /* Non Zero */ long nGetReleaseFunc();
146 
147     @CriticalNative
nGetMemoryUsage( long nativePtr)148     private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
149 
nGetBounds(long nativePtr, char[] buf, int start, int end, Rect rect)150     private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
151             Rect rect);
152 
153     @CriticalNative
nGetCharWidthAt(long nativePtr, int offset)154     private static native float nGetCharWidthAt(long nativePtr, int offset);
155 
156     /**
157      * Helper class for creating a {@link MeasuredText}.
158      * <p>
159      * <pre>
160      * <code>
161      * Paint paint = new Paint();
162      * String text = "Hello, Android.";
163      * MeasuredText mt = new MeasuredText.Builder(text.toCharArray())
164      *      .appendStyleRun(paint, text.length, false)
165      *      .build();
166      * </code>
167      * </pre>
168      * </p>
169      *
170      * Note: The appendStyle and appendReplacementRun should be called to cover the text length.
171      */
172     public static final class Builder {
173         private static final NativeAllocationRegistry sRegistry =
174                 NativeAllocationRegistry.createMalloced(
175                 MeasuredText.class.getClassLoader(), nGetReleaseFunc());
176 
177         private long mNativePtr;
178 
179         private final @NonNull char[] mText;
180         private boolean mComputeHyphenation = false;
181         private boolean mComputeLayout = true;
182         private int mCurrentOffset = 0;
183         private @Nullable MeasuredText mHintMt = null;
184 
185         /**
186          * Construct a builder.
187          *
188          * The MeasuredText returned by build method will hold a reference of the text. Developer is
189          * not supposed to modify the text.
190          *
191          * @param text a text
192          */
Builder(@onNull char[] text)193         public Builder(@NonNull char[] text) {
194             Preconditions.checkNotNull(text);
195             mText = text;
196             mNativePtr = nInitBuilder();
197         }
198 
199         /**
200          * Construct a builder with existing MeasuredText.
201          *
202          * The MeasuredText returned by build method will hold a reference of the text. Developer is
203          * not supposed to modify the text.
204          *
205          * @param text a text
206          */
Builder(@onNull MeasuredText text)207         public Builder(@NonNull MeasuredText text) {
208             Preconditions.checkNotNull(text);
209             mText = text.mChars;
210             mNativePtr = nInitBuilder();
211             if (!text.mComputeLayout) {
212                 throw new IllegalArgumentException(
213                     "The input MeasuredText must not be created with setComputeLayout(false).");
214             }
215             mComputeHyphenation = text.mComputeHyphenation;
216             mComputeLayout = text.mComputeLayout;
217             mHintMt = text;
218         }
219 
220         /**
221          * Apply styles to the given length.
222          *
223          * Keeps an internal offset which increases at every append. The initial value for this
224          * offset is zero. After the style is applied the internal offset is moved to {@code offset
225          * + length}, and next call will start from this new position.
226          *
227          * @param paint a paint
228          * @param length a length to be applied with a given paint, can not exceed the length of the
229          *               text
230          * @param isRtl true if the text is in RTL context, otherwise false.
231          */
appendStyleRun(@onNull Paint paint, @IntRange(from = 0) int length, boolean isRtl)232         public @NonNull Builder appendStyleRun(@NonNull Paint paint, @IntRange(from = 0) int length,
233                 boolean isRtl) {
234             Preconditions.checkNotNull(paint);
235             Preconditions.checkArgument(length > 0, "length can not be negative");
236             final int end = mCurrentOffset + length;
237             Preconditions.checkArgument(end <= mText.length, "Style exceeds the text length");
238             nAddStyleRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, isRtl);
239             mCurrentOffset = end;
240             return this;
241         }
242 
243         /**
244          * Used to inform the text layout that the given length is replaced with the object of given
245          * width.
246          *
247          * Keeps an internal offset which increases at every append. The initial value for this
248          * offset is zero. After the style is applied the internal offset is moved to {@code offset
249          * + length}, and next call will start from this new position.
250          *
251          * Informs the layout engine that the given length should not be processed, instead the
252          * provided width should be used for calculating the width of that range.
253          *
254          * @param length a length to be replaced with the object, can not exceed the length of the
255          *               text
256          * @param width a replacement width of the range
257          */
appendReplacementRun(@onNull Paint paint, @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width)258         public @NonNull Builder appendReplacementRun(@NonNull Paint paint,
259                 @IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width) {
260             Preconditions.checkArgument(length > 0, "length can not be negative");
261             final int end = mCurrentOffset + length;
262             Preconditions.checkArgument(end <= mText.length, "Replacement exceeds the text length");
263             nAddReplacementRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, width);
264             mCurrentOffset = end;
265             return this;
266         }
267 
268         /**
269          * By passing true to this method, the build method will compute all possible hyphenation
270          * pieces as well.
271          *
272          * If you don't want to use automatic hyphenation, you can pass false to this method and
273          * save the computation time of hyphenation. The default value is false.
274          *
275          * Even if you pass false to this method, you can still enable automatic hyphenation of
276          * LineBreaker but line break computation becomes slower.
277          *
278          * @param computeHyphenation true if you want to use automatic hyphenations.
279          */
setComputeHyphenation(boolean computeHyphenation)280         public @NonNull Builder setComputeHyphenation(boolean computeHyphenation) {
281             mComputeHyphenation = computeHyphenation;
282             return this;
283         }
284 
285         /**
286          * By passing true to this method, the build method will compute all full layout
287          * information.
288          *
289          * If you don't use {@link MeasuredText#getBounds(int,int,android.graphics.Rect)}, you can
290          * pass false to this method and save the memory spaces. The default value is true.
291          *
292          * Even if you pass false to this method, you can still call getBounds but it becomes
293          * slower.
294          *
295          * @param computeLayout true if you want to retrieve full layout info, e.g. bbox.
296          */
setComputeLayout(boolean computeLayout)297         public @NonNull Builder setComputeLayout(boolean computeLayout) {
298             mComputeLayout = computeLayout;
299             return this;
300         }
301 
302         /**
303          * Creates a MeasuredText.
304          *
305          * Once you called build() method, you can't reuse the Builder class again.
306          * @throws IllegalStateException if this Builder is reused.
307          * @throws IllegalStateException if the whole text is not covered by one or more runs (style
308          *                               or replacement)
309          */
build()310         public @NonNull MeasuredText build() {
311             ensureNativePtrNoReuse();
312             if (mCurrentOffset != mText.length) {
313                 throw new IllegalStateException("Style info has not been provided for all text.");
314             }
315             if (mHintMt != null && mHintMt.mComputeHyphenation != mComputeHyphenation) {
316                 throw new IllegalArgumentException(
317                         "The hyphenation configuration is different from given hint MeasuredText");
318             }
319             try {
320                 long hintPtr = (mHintMt == null) ? 0 : mHintMt.getNativePtr();
321                 long ptr = nBuildMeasuredText(mNativePtr, hintPtr, mText, mComputeHyphenation,
322                         mComputeLayout);
323                 final MeasuredText res = new MeasuredText(ptr, mText, mComputeHyphenation,
324                         mComputeLayout);
325                 sRegistry.registerNativeAllocation(res, ptr);
326                 return res;
327             } finally {
328                 nFreeBuilder(mNativePtr);
329                 mNativePtr = 0;
330             }
331         }
332 
333         /**
334          * Ensures {@link #mNativePtr} is not reused.
335          *
336          * <p/> This is a method by itself to help increase testability - eg. Robolectric might want
337          * to override the validation behavior in test environment.
338          */
ensureNativePtrNoReuse()339         private void ensureNativePtrNoReuse() {
340             if (mNativePtr == 0) {
341                 throw new IllegalStateException("Builder can not be reused.");
342             }
343         }
344 
nInitBuilder()345         private static native /* Non Zero */ long nInitBuilder();
346 
347         /**
348          * Apply style to make native measured text.
349          *
350          * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
351          * @param paintPtr The native paint pointer to be applied.
352          * @param start The start offset in the copied buffer.
353          * @param end The end offset in the copied buffer.
354          * @param isRtl True if the text is RTL.
355          */
nAddStyleRun( long nativeBuilderPtr, long paintPtr, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl)356         private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
357                                                 /* Non Zero */ long paintPtr,
358                                                 @IntRange(from = 0) int start,
359                                                 @IntRange(from = 0) int end,
360                                                 boolean isRtl);
361         /**
362          * Apply ReplacementRun to make native measured text.
363          *
364          * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
365          * @param paintPtr The native paint pointer to be applied.
366          * @param start The start offset in the copied buffer.
367          * @param end The end offset in the copied buffer.
368          * @param width The width of the replacement.
369          */
nAddReplacementRun( long nativeBuilderPtr, long paintPtr, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @FloatRange(from = 0) float width)370         private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
371                                                       /* Non Zero */ long paintPtr,
372                                                       @IntRange(from = 0) int start,
373                                                       @IntRange(from = 0) int end,
374                                                       @FloatRange(from = 0) float width);
375 
nBuildMeasuredText( long nativeBuilderPtr, long hintMtPtr, @NonNull char[] text, boolean computeHyphenation, boolean computeLayout)376         private static native long nBuildMeasuredText(
377                 /* Non Zero */ long nativeBuilderPtr,
378                 long hintMtPtr,
379                 @NonNull char[] text,
380                 boolean computeHyphenation,
381                 boolean computeLayout);
382 
nFreeBuilder( long nativeBuilderPtr)383         private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
384     }
385 }
386