From 70616ecd22fafccf2fab7565ccfbb3b5f91c5580 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 4 Mar 2015 10:41:30 -0800 Subject: [PATCH] Start moving text measurement into native code We want to move text measurement into native code, mostly so that alternate measurement for hyphens can be performant. This patch begins that migration, in the main StaticLayout case, but still surfaces the widths array to Java (for ellipsis calculation), and also includes a hack (used mostly for testing) for computing widths in Java and sending them down to native code when TextPaint is subclassed. Change-Id: I476c9e8b3aa8e4e3552eb18f66c4bcd5683f3a72 --- core/java/android/text/Layout.java | 2 +- core/java/android/text/MeasuredText.java | 46 +++++++++-- core/java/android/text/StaticLayout.java | 76 ++++++++++++++----- core/java/android/text/TextUtils.java | 4 +- core/jni/android_text_StaticLayout.cpp | 97 +++++++++++++++++++++--- 5 files changed, 184 insertions(+), 41 deletions(-) diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index b84c3aa9b05b3..fcf1828d734de 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -1564,7 +1564,7 @@ public abstract class Layout { MeasuredText mt = MeasuredText.obtain(); TextLine tl = TextLine.obtain(); try { - mt.setPara(text, start, end, TextDirectionHeuristics.LTR); + mt.setPara(text, start, end, TextDirectionHeuristics.LTR, null); Directions directions; int dir; if (mt.mEasy) { diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index 832002c94e6c6..55df2069ea353 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -39,6 +39,7 @@ class MeasuredText { private int mPos; private TextPaint mWorkPaint; + private StaticLayout.Builder mBuilder; private MeasuredText() { mWorkPaint = new TextPaint(); @@ -81,6 +82,7 @@ class MeasuredText { void finish() { mText = null; + mBuilder = null; if (mLen > 1000) { mWidths = null; mChars = null; @@ -95,7 +97,9 @@ class MeasuredText { /** * Analyzes text for bidirectional runs. Allocates working buffers. */ - void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) { + void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir, + StaticLayout.Builder builder) { + mBuilder = builder; mText = text; mTextStart = start; @@ -164,9 +168,24 @@ class MeasuredText { int p = mPos; mPos = p + len; + // try to do widths measurement in native code, but use Java if paint has been subclassed + // FIXME: may want to eliminate special case for subclass + float[] widths = null; + if (mBuilder == null || paint.getClass() != TextPaint.class) { + widths = mWidths; + } if (mEasy) { boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT; - return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p); + float width = 0; + if (widths != null) { + width = paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, widths, p); + if (mBuilder != null) { + mBuilder.addMeasuredRun(p, p + len, widths); + } + } else { + width = mBuilder.addStyleRun(paint, p, p + len, isRtl); + } + return width; } float totalAdvance = 0; @@ -174,8 +193,15 @@ class MeasuredText { for (int q = p, i = p + 1, e = p + len;; ++i) { if (i == e || mLevels[i] != level) { boolean isRtl = (level & 0x1) != 0; - totalAdvance += - paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q); + if (widths != null) { + totalAdvance += + paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, widths, q); + if (mBuilder != null) { + mBuilder.addMeasuredRun(q, i, widths); + } + } else { + totalAdvance += mBuilder.addStyleRun(paint, q, i, isRtl); + } if (i == e) { break; } @@ -211,10 +237,14 @@ class MeasuredText { // Use original text. Shouldn't matter. wid = replacement.getSize(workPaint, mText, mTextStart + mPos, mTextStart + mPos + len, fm); - float[] w = mWidths; - w[mPos] = wid; - for (int i = mPos + 1, e = mPos + len; i < e; i++) - w[i] = 0; + if (mBuilder == null) { + float[] w = mWidths; + w[mPos] = wid; + for (int i = mPos + 1, e = mPos + len; i < e; i++) + w[i] = 0; + } else { + mBuilder.addReplacementRun(mPos, mPos + len, wid); + } mPos += len; } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 0d35f9c1567aa..ee39e27dfe770 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -166,18 +166,48 @@ public class StaticLayout extends Layout { return this; } - /* @hide */ - public void setLocale(Locale locale) { + /** + * Measurement and break iteration is done in native code. The protocol for using + * the native code is as follows. + * + * For each paragraph, do a nSetText of the paragraph text. Then, for each run within the + * paragraph: + * - setLocale (this must be done at least for the first run, optional afterwards) + * - one of the following, depending on the type of run: + * + addStyleRun (a text run, to be measured in native code) + * + addMeasuredRun (a run already measured in Java, passed into native code) + * + addReplacementRun (a replacement run, width is given) + * + * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis). + * Run nComputeLineBreaks() to obtain line breaks for the paragraph. + * + * After all paragraphs, call finish() to release expensive buffers. + */ + + private void setLocale(Locale locale) { if (!locale.equals(mLocale)) { - nBuilderSetLocale(mNativePtr, locale.toLanguageTag()); + nSetLocale(mNativePtr, locale.toLanguageTag()); mLocale = locale; } } + /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { + return nAddStyleRun(mNativePtr, paint.getNativeInstance(), paint.mNativeTypeface, + start, end, isRtl); + } + + /* package */ void addMeasuredRun(int start, int end, float[] widths) { + nAddMeasuredRun(mNativePtr, start, end, widths); + } + + /* package */ void addReplacementRun(int start, int end, float width) { + nAddReplacementRun(mNativePtr, start, end, width); + } + public StaticLayout build() { // TODO: can optimize based on whether ellipsis is needed StaticLayout result = new StaticLayout(mText); - result.initFromBuilder(this); + result.generate(this, this.mIncludePad, this.mIncludePad); recycle(this); return result; } @@ -321,7 +351,7 @@ public class StaticLayout extends Layout { mLines = new int[mLineDirections.length]; mMaximumVisibleLineCount = maxLines; - initFromBuilder(b); + generate(b, b.mIncludePad, b.mIncludePad); Builder.recycle(b); } @@ -334,10 +364,6 @@ public class StaticLayout extends Layout { mLines = new int[mLineDirections.length]; } - private void initFromBuilder(Builder b) { - generate(b, b.mIncludePad, b.mIncludePad); - } - /* package */ void generate(Builder b, boolean includepad, boolean trackpad) { CharSequence source = b.mText; int bufStart = b.mStart; @@ -427,12 +453,13 @@ public class StaticLayout extends Layout { } } - measured.setPara(source, paraStart, paraEnd, textDir); + measured.setPara(source, paraStart, paraEnd, textDir, b); char[] chs = measured.mChars; float[] widths = measured.mWidths; byte[] chdirs = measured.mLevels; int dir = measured.mDir; boolean easy = measured.mEasy; + nSetText(b.mNativePtr, chs, paraEnd - paraStart); // measurement has to be done before performing line breaking // but we don't want to recompute fontmetrics or span ranges the @@ -493,7 +520,8 @@ public class StaticLayout extends Layout { } } - int breakCount = nComputeLineBreaks(b.mNativePtr, chs, widths, paraEnd - paraStart, firstWidth, + nGetWidths(b.mNativePtr, widths); + int breakCount = nComputeLineBreaks(b.mNativePtr, paraEnd - paraStart, firstWidth, firstWidthLineCount, restWidth, variableTabStops, TAB_INCREMENT, false, lineBreaks, lineBreaks.breaks, lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); @@ -576,7 +604,7 @@ public class StaticLayout extends Layout { mLineCount < mMaximumVisibleLineCount) { // Log.e("text", "output last " + bufEnd); - measured.setPara(source, bufEnd, bufEnd, textDir); + measured.setPara(source, bufEnd, bufEnd, textDir, b); paint.getFontMetricsInt(fm); @@ -933,21 +961,33 @@ public class StaticLayout extends Layout { return mEllipsizedWidth; } + private static native long nNewBuilder(); + private static native void nFreeBuilder(long nativePtr); + private static native void nFinishBuilder(long nativePtr); + private static native void nSetLocale(long nativePtr, String locale); + + private static native void nSetText(long nativePtr, char[] text, int length); + + private static native float nAddStyleRun(long nativePtr, long nativePaint, + long nativeTypeface, int start, int end, boolean isRtl); + + private static native void nAddMeasuredRun(long nativePtr, + int start, int end, float[] widths); + + private static native void nAddReplacementRun(long nativePtr, int start, int end, float width); + + private static native void nGetWidths(long nativePtr, float[] widths); + // populates LineBreaks and returns the number of breaks found // // the arrays inside the LineBreaks objects are passed in as well // to reduce the number of JNI calls in the common case where the // arrays do not have to be resized - private static native int nComputeLineBreaks(long nativePtr, char[] text, float[] widths, + private static native int nComputeLineBreaks(long nativePtr, int length, float firstWidth, int firstWidthLineCount, float restWidth, int[] variableTabStops, int defaultTabStop, boolean optimize, LineBreaks recycle, int[] recycleBreaks, float[] recycleWidths, boolean[] recycleFlags, int recycleLength); - private static native long nNewBuilder(); - private static native void nFreeBuilder(long nativePtr); - private static native void nFinishBuilder(long nativePtr); - private static native void nBuilderSetLocale(long nativePtr, String locale); - private int mLineCount; private int mTopPadding, mBottomPadding; private int mColumns; diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 1bb35f6bed35f..676986d2335bb 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -1259,7 +1259,7 @@ public class TextUtils { } // XXX this is probably ok, but need to look at it more - tempMt.setPara(format, 0, format.length(), textDir); + tempMt.setPara(format, 0, format.length(), textDir, null); float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null); if (w + moreWid <= avail) { @@ -1281,7 +1281,7 @@ public class TextUtils { private static float setPara(MeasuredText mt, TextPaint paint, CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - mt.setPara(text, start, end, textDir); + mt.setPara(text, start, end, textDir, null); float width; Spanned sp = text instanceof Spanned ? (Spanned) text : null; diff --git a/core/jni/android_text_StaticLayout.cpp b/core/jni/android_text_StaticLayout.cpp index e5ae14770c23f..8800f0bc3184a 100644 --- a/core/jni/android_text_StaticLayout.cpp +++ b/core/jni/android_text_StaticLayout.cpp @@ -29,6 +29,11 @@ #include #include +#include "SkPaint.h" +#include "SkTypeface.h" +#include "MinikinSkia.h" +#include "MinikinUtils.h" +#include "Paint.h" namespace android { @@ -57,12 +62,21 @@ class Builder { void resize(size_t size) { mTextBuf.resize(size); + mWidthBuf.resize(size); + } + + size_t size() const { + return mTextBuf.size(); } uint16_t* buffer() { return mTextBuf.data(); } + float* widths() { + return mWidthBuf.data(); + } + // set text to current contents of buffer void setText() { UErrorCode status = U_ZERO_ERROR; @@ -74,6 +88,8 @@ class Builder { if (mTextBuf.size() > MAX_TEXT_BUF_RETAIN) { mTextBuf.clear(); mTextBuf.shrink_to_fit(); + mWidthBuf.clear(); + mWidthBuf.shrink_to_fit(); } } @@ -81,11 +97,17 @@ class Builder { return mBreakIterator; } + float measureStyleRun(Paint* paint, TypefaceImpl* typeface, size_t start, size_t end, + bool isRtl); + + void addReplacement(size_t start, size_t end, float width); + private: const size_t MAX_TEXT_BUF_RETAIN = 32678; icu::BreakIterator* mBreakIterator = nullptr; UText mUText = UTEXT_INITIALIZER; std::vectormTextBuf; + std::vectormWidthBuf; }; static const int CHAR_SPACE = 0x20; @@ -543,22 +565,24 @@ void computePrimitives(const jchar* textArr, const jfloat* widthsArr, jint lengt primitives->push_back(p); } +// sets the text on the builder +static void nSetText(JNIEnv* env, jclass, jlong nativePtr, jcharArray text, int length) { + Builder* b = reinterpret_cast(nativePtr); + b->resize(length); + env->GetCharArrayRegion(text, 0, length, b->buffer()); + b->setText(); +} + static jint nComputeLineBreaks(JNIEnv* env, jclass, jlong nativePtr, - jcharArray inputText, jfloatArray widths, jint length, + jint length, jfloat firstWidth, jint firstWidthLineLimit, jfloat restWidth, jintArray variableTabStops, jint defaultTabStop, jboolean optimize, jobject recycle, jintArray recycleBreaks, jfloatArray recycleWidths, jbooleanArray recycleFlags, jint recycleLength) { - std::vector breaks; - Builder* b = reinterpret_cast(nativePtr); - b->resize(length); - env->GetCharArrayRegion(inputText, 0, length, b->buffer()); - b->setText(); - // TODO: this array access is pretty inefficient, but we'll replace it anyway - ScopedFloatArrayRO widthsScopedArr(env, widths); + std::vector breaks; icu::BreakIterator* breakIterator = b->breakIterator(); int loc = breakIterator->first(); @@ -569,7 +593,7 @@ static jint nComputeLineBreaks(JNIEnv* env, jclass, jlong nativePtr, // TODO: all these allocations can be moved into the builder std::vector primitives; TabStops tabStops(env, variableTabStops, defaultTabStop); - computePrimitives(b->buffer(), widthsScopedArr.get(), length, breaks, tabStops, &primitives); + computePrimitives(b->buffer(), b->widths(), length, breaks, tabStops, &primitives); LineWidth lineWidth(firstWidth, firstWidthLineLimit, restWidth); std::vector computedBreaks; @@ -602,7 +626,7 @@ static void nFinishBuilder(JNIEnv*, jclass, jlong nativePtr) { b->finish(); } -static void nBuilderSetLocale(JNIEnv* env, jclass, jlong nativePtr, jstring javaLocaleName) { +static void nSetLocale(JNIEnv* env, jclass, jlong nativePtr, jstring javaLocaleName) { ScopedIcuLocale icuLocale(env, javaLocaleName); Builder* b = reinterpret_cast(nativePtr); @@ -611,12 +635,61 @@ static void nBuilderSetLocale(JNIEnv* env, jclass, jlong nativePtr, jstring java } } +float Builder::measureStyleRun(Paint* paint, TypefaceImpl* typeface, size_t start, size_t end, + bool isRtl) { + Layout layout; + int bidiFlags = isRtl ? kBidi_Force_RTL : kBidi_Force_LTR; + // TODO: should we provide more context? + MinikinUtils::doLayout(&layout, paint, bidiFlags, typeface, mTextBuf.data() + start, 0, + end - start, end - start); + layout.getAdvances(mWidthBuf.data() + start); + return layout.getAdvance(); +} + +void Builder::addReplacement(size_t start, size_t end, float width) { + mWidthBuf[start] = width; + std::fill(&mWidthBuf[start + 1], &mWidthBuf[end], 0.0f); +} + +// Basically similar to Paint.getTextRunAdvances but with C++ interface +static jfloat nAddStyleRun(JNIEnv* env, jclass, jlong nativePtr, + jlong nativePaint, jlong nativeTypeface, jint start, jint end, jboolean isRtl) { + Builder* b = reinterpret_cast(nativePtr); + Paint* paint = reinterpret_cast(nativePaint); + TypefaceImpl* typeface = reinterpret_cast(nativeTypeface); + return b->measureStyleRun(paint, typeface, start, end, isRtl); +} + +// Accept width measurements for the run, passed in from Java +static void nAddMeasuredRun(JNIEnv* env, jclass, jlong nativePtr, + jint start, jint end, jfloatArray widths) { + Builder* b = reinterpret_cast(nativePtr); + env->GetFloatArrayRegion(widths, start, end - start, b->widths() + start); +} + +static void nAddReplacementRun(JNIEnv* env, jclass, jlong nativePtr, + jint start, jint end, jfloat width) { + Builder* b = reinterpret_cast(nativePtr); + b->addReplacement(start, end, width); +} + +static void nGetWidths(JNIEnv* env, jclass, jlong nativePtr, jfloatArray widths) { + Builder* b = reinterpret_cast(nativePtr); + env->SetFloatArrayRegion(widths, 0, b->size(), b->widths()); +} + static JNINativeMethod gMethods[] = { + // TODO performance: many of these are candidates for fast jni, awaiting guidance {"nNewBuilder", "()J", (void*) nNewBuilder}, {"nFreeBuilder", "(J)V", (void*) nFreeBuilder}, {"nFinishBuilder", "(J)V", (void*) nFinishBuilder}, - {"nBuilderSetLocale", "(JLjava/lang/String;)V", (void*) nBuilderSetLocale}, - {"nComputeLineBreaks", "(J[C[FIFIF[IIZLandroid/text/StaticLayout$LineBreaks;[I[F[ZI)I", + {"nSetLocale", "(JLjava/lang/String;)V", (void*) nSetLocale}, + {"nSetText", "(J[CI)V", (void*) nSetText}, + {"nAddStyleRun", "(JJJIIZ)F", (void*) nAddStyleRun}, + {"nAddMeasuredRun", "(JII[F)V", (void*) nAddMeasuredRun}, + {"nAddReplacementRun", "(JIIF)V", (void*) nAddReplacementRun}, + {"nGetWidths", "(J[F)V", (void*) nGetWidths}, + {"nComputeLineBreaks", "(JIFIF[IIZLandroid/text/StaticLayout$LineBreaks;[I[F[ZI)I", (void*) nComputeLineBreaks} };