/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.graphics.text; import android.annotation.FloatRange; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Px; import android.graphics.Paint; import android.graphics.Rect; import com.android.internal.util.Preconditions; import dalvik.annotation.optimization.CriticalNative; import libcore.util.NativeAllocationRegistry; /** * Result of text shaping of the single paragraph string. * *
*
*
* Paint paint = new Paint();
* Paint bigPaint = new Paint();
* bigPaint.setTextSize(paint.getTextSize() * 2.0);
* String text = "Hello, Android.";
* MeasuredText mt = new MeasuredText.Builder(text.toCharArray())
* .appendStyleRun(paint, 7, false) // Use paint for "Hello, "
* .appendStyleRun(bigPaint, 8, false) // Use bigPaint for "Android."
* .build();
*
*
*
*/
public class MeasuredText {
private long mNativePtr;
private boolean mComputeHyphenation;
private boolean mComputeLayout;
private @NonNull char[] mChars;
// Use builder instead.
private MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation,
boolean computeLayout) {
mNativePtr = ptr;
mChars = chars;
mComputeHyphenation = computeHyphenation;
mComputeLayout = computeLayout;
}
/**
* Returns the characters in the paragraph used to compute this MeasuredText instance.
* @hide
*/
public @NonNull char[] getChars() {
return mChars;
}
/**
* Returns the width of a given range.
*
* @param start an inclusive start index of the range
* @param end an exclusive end index of the range
*/
public @FloatRange(from = 0.0) @Px float getWidth(
@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
Preconditions.checkArgument(0 <= start && start <= mChars.length,
"start(" + start + ") must be 0 <= start <= " + mChars.length);
Preconditions.checkArgument(0 <= end && end <= mChars.length,
"end(" + end + ") must be 0 <= end <= " + mChars.length);
Preconditions.checkArgument(start <= end,
"start(" + start + ") is larger than end(" + end + ")");
return nGetWidth(mNativePtr, start, end);
}
/**
* Returns a memory usage of the native object.
*
* @hide
*/
public int getMemoryUsage() {
return nGetMemoryUsage(mNativePtr);
}
/**
* Retrieves the boundary box of the given range
*
* @param start an inclusive start index of the range
* @param end an exclusive end index of the range
* @param rect an output parameter
*/
public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
@NonNull Rect rect) {
Preconditions.checkArgument(0 <= start && start <= mChars.length,
"start(" + start + ") must be 0 <= start <= " + mChars.length);
Preconditions.checkArgument(0 <= end && end <= mChars.length,
"end(" + end + ") must be 0 <= end <= " + mChars.length);
Preconditions.checkArgument(start <= end,
"start(" + start + ") is larger than end(" + end + ")");
Preconditions.checkNotNull(rect);
nGetBounds(mNativePtr, mChars, start, end, rect);
}
/**
* Returns the width of the character at the given offset.
*
* @param offset an offset of the character.
*/
public @FloatRange(from = 0.0f) @Px float getCharWidthAt(@IntRange(from = 0) int offset) {
Preconditions.checkArgument(0 <= offset && offset < mChars.length,
"offset(" + offset + ") is larger than text length: " + mChars.length);
return nGetCharWidthAt(mNativePtr, offset);
}
/**
* Returns a native pointer of the underlying native object.
*
* @hide
*/
public long getNativePtr() {
return mNativePtr;
}
@CriticalNative
private static native float nGetWidth(/* Non Zero */ long nativePtr,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end);
@CriticalNative
private static native /* Non Zero */ long nGetReleaseFunc();
@CriticalNative
private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
Rect rect);
@CriticalNative
private static native float nGetCharWidthAt(long nativePtr, int offset);
/**
* Helper class for creating a {@link MeasuredText}.
* *
*
* Paint paint = new Paint();
* String text = "Hello, Android.";
* MeasuredText mt = new MeasuredText.Builder(text.toCharArray())
* .appendStyleRun(paint, text.length, false)
* .build();
*
*
*
*
* Note: The appendStyle and appendReplacementRun should be called to cover the text length.
*/
public static final class Builder {
private static final NativeAllocationRegistry sRegistry =
NativeAllocationRegistry.createMalloced(
MeasuredText.class.getClassLoader(), nGetReleaseFunc());
private long mNativePtr;
private final @NonNull char[] mText;
private boolean mComputeHyphenation = false;
private boolean mComputeLayout = true;
private int mCurrentOffset = 0;
private @Nullable MeasuredText mHintMt = null;
/**
* Construct a builder.
*
* The MeasuredText returned by build method will hold a reference of the text. Developer is
* not supposed to modify the text.
*
* @param text a text
*/
public Builder(@NonNull char[] text) {
Preconditions.checkNotNull(text);
mText = text;
mNativePtr = nInitBuilder();
}
/**
* Construct a builder with existing MeasuredText.
*
* The MeasuredText returned by build method will hold a reference of the text. Developer is
* not supposed to modify the text.
*
* @param text a text
*/
public Builder(@NonNull MeasuredText text) {
Preconditions.checkNotNull(text);
mText = text.mChars;
mNativePtr = nInitBuilder();
if (!text.mComputeLayout) {
throw new IllegalArgumentException(
"The input MeasuredText must not be created with setComputeLayout(false).");
}
mComputeHyphenation = text.mComputeHyphenation;
mComputeLayout = text.mComputeLayout;
mHintMt = text;
}
/**
* Apply styles to the given length.
*
* Keeps an internal offset which increases at every append. The initial value for this
* offset is zero. After the style is applied the internal offset is moved to {@code offset
* + length}, and next call will start from this new position.
*
* @param paint a paint
* @param length a length to be applied with a given paint, can not exceed the length of the
* text
* @param isRtl true if the text is in RTL context, otherwise false.
*/
public @NonNull Builder appendStyleRun(@NonNull Paint paint, @IntRange(from = 0) int length,
boolean isRtl) {
Preconditions.checkNotNull(paint);
Preconditions.checkArgument(length > 0, "length can not be negative");
final int end = mCurrentOffset + length;
Preconditions.checkArgument(end <= mText.length, "Style exceeds the text length");
nAddStyleRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, isRtl);
mCurrentOffset = end;
return this;
}
/**
* Used to inform the text layout that the given length is replaced with the object of given
* width.
*
* Keeps an internal offset which increases at every append. The initial value for this
* offset is zero. After the style is applied the internal offset is moved to {@code offset
* + length}, and next call will start from this new position.
*
* Informs the layout engine that the given length should not be processed, instead the
* provided width should be used for calculating the width of that range.
*
* @param length a length to be replaced with the object, can not exceed the length of the
* text
* @param width a replacement width of the range
*/
public @NonNull Builder appendReplacementRun(@NonNull Paint paint,
@IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width) {
Preconditions.checkArgument(length > 0, "length can not be negative");
final int end = mCurrentOffset + length;
Preconditions.checkArgument(end <= mText.length, "Replacement exceeds the text length");
nAddReplacementRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, width);
mCurrentOffset = end;
return this;
}
/**
* By passing true to this method, the build method will compute all possible hyphenation
* pieces as well.
*
* If you don't want to use automatic hyphenation, you can pass false to this method and
* save the computation time of hyphenation. The default value is false.
*
* Even if you pass false to this method, you can still enable automatic hyphenation of
* LineBreaker but line break computation becomes slower.
*
* @param computeHyphenation true if you want to use automatic hyphenations.
*/
public @NonNull Builder setComputeHyphenation(boolean computeHyphenation) {
mComputeHyphenation = computeHyphenation;
return this;
}
/**
* By passing true to this method, the build method will compute all full layout
* information.
*
* If you don't use {@link MeasuredText#getBounds(int,int,android.graphics.Rect)}, you can
* pass false to this method and save the memory spaces. The default value is true.
*
* Even if you pass false to this method, you can still call getBounds but it becomes
* slower.
*
* @param computeLayout true if you want to retrieve full layout info, e.g. bbox.
*/
public @NonNull Builder setComputeLayout(boolean computeLayout) {
mComputeLayout = computeLayout;
return this;
}
/**
* Creates a MeasuredText.
*
* Once you called build() method, you can't reuse the Builder class again.
* @throws IllegalStateException if this Builder is reused.
* @throws IllegalStateException if the whole text is not covered by one or more runs (style
* or replacement)
*/
public @NonNull MeasuredText build() {
ensureNativePtrNoReuse();
if (mCurrentOffset != mText.length) {
throw new IllegalStateException("Style info has not been provided for all text.");
}
if (mHintMt != null && mHintMt.mComputeHyphenation != mComputeHyphenation) {
throw new IllegalArgumentException(
"The hyphenation configuration is different from given hint MeasuredText");
}
try {
long hintPtr = (mHintMt == null) ? 0 : mHintMt.getNativePtr();
long ptr = nBuildMeasuredText(mNativePtr, hintPtr, mText, mComputeHyphenation,
mComputeLayout);
final MeasuredText res = new MeasuredText(ptr, mText, mComputeHyphenation,
mComputeLayout);
sRegistry.registerNativeAllocation(res, ptr);
return res;
} finally {
nFreeBuilder(mNativePtr);
mNativePtr = 0;
}
}
/**
* Ensures {@link #mNativePtr} is not reused.
*
* This is a method by itself to help increase testability - eg. Robolectric might want
* to override the validation behavior in test environment.
*/
private void ensureNativePtrNoReuse() {
if (mNativePtr == 0) {
throw new IllegalStateException("Builder can not be reused.");
}
}
private static native /* Non Zero */ long nInitBuilder();
/**
* Apply style to make native measured text.
*
* @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
* @param paintPtr The native paint pointer to be applied.
* @param start The start offset in the copied buffer.
* @param end The end offset in the copied buffer.
* @param isRtl True if the text is RTL.
*/
private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
/* Non Zero */ long paintPtr,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
boolean isRtl);
/**
* Apply ReplacementRun to make native measured text.
*
* @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
* @param paintPtr The native paint pointer to be applied.
* @param start The start offset in the copied buffer.
* @param end The end offset in the copied buffer.
* @param width The width of the replacement.
*/
private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
/* Non Zero */ long paintPtr,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@FloatRange(from = 0) float width);
private static native long nBuildMeasuredText(
/* Non Zero */ long nativeBuilderPtr,
long hintMtPtr,
@NonNull char[] text,
boolean computeHyphenation,
boolean computeLayout);
private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
}
}