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