package android.text; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.annotation.NonNull; import android.graphics.BidiRenderer; import android.graphics.Paint; import android.graphics.Paint_Delegate; import android.graphics.RectF; import android.icu.text.BreakIterator; import android.icu.util.ULocale; import android.text.Primitive.PrimitiveType; import android.text.StaticLayout.LineBreaks; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.swing.text.Segment; /** * Delegate that provides implementation for native methods in {@link android.text.StaticLayout} *

* Through the layoutlib_create tool, selected methods of StaticLayout have been replaced * by calls to methods of the same name in this delegate class. * */ public class StaticLayout_Delegate { private static final char CHAR_SPACE = 0x20; private static final char CHAR_TAB = 0x09; private static final char CHAR_NEWLINE = 0x0A; private static final char CHAR_ZWSP = 0x200B; // Zero width space. // ---- Builder delegate manager ---- private static final DelegateManager sBuilderManager = new DelegateManager(Builder.class); @LayoutlibDelegate /*package*/ static long nNewBuilder() { return sBuilderManager.addNewDelegate(new Builder()); } @LayoutlibDelegate /*package*/ static void nFreeBuilder(long nativeBuilder) { sBuilderManager.removeJavaReferenceFor(nativeBuilder); } @LayoutlibDelegate /*package*/ static void nFinishBuilder(long nativeBuilder) { } @LayoutlibDelegate /*package*/ static long nLoadHyphenator(ByteBuffer buf, int offset, int minPrefix, int minSuffix) { return Hyphenator_Delegate.loadHyphenator(buf, offset, minPrefix, minSuffix); } @LayoutlibDelegate /*package*/ static void nSetLocales(long nativeBuilder, String locales, long[] nativeHyphenators) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder != null) { builder.mLocales = locales; builder.mNativeHyphenators = nativeHyphenators; } } @LayoutlibDelegate /*package*/ static void nSetIndents(long nativeBuilder, int[] indents) { // TODO. } @LayoutlibDelegate /*package*/ static void nSetupParagraph(long nativeBuilder, char[] text, int length, float firstWidth, int firstWidthLineCount, float restWidth, int[] variableTabStops, int defaultTabStop, int breakStrategy, int hyphenationFrequency, boolean isJustified) { // TODO: implement justified alignment Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder == null) { return; } builder.mText = text; builder.mWidths = new float[length]; builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth); builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop); } @LayoutlibDelegate /*package*/ static float nAddStyleRun(long nativeBuilder, long nativePaint, long nativeTypeface, int start, int end, boolean isRtl) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); int bidiFlags = isRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR; return builder == null ? 0 : measureText(nativePaint, builder.mText, start, end - start, builder.mWidths, bidiFlags); } @LayoutlibDelegate /*package*/ static void nAddMeasuredRun(long nativeBuilder, int start, int end, float[] widths) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder != null) { System.arraycopy(widths, start, builder.mWidths, start, end - start); } } @LayoutlibDelegate /*package*/ static void nAddReplacementRun(long nativeBuilder, int start, int end, float width) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder == null) { return; } builder.mWidths[start] = width; Arrays.fill(builder.mWidths, start + 1, end, 0.0f); } @LayoutlibDelegate /*package*/ static void nGetWidths(long nativeBuilder, float[] floatsArray) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder != null) { System.arraycopy(builder.mWidths, 0, floatsArray, 0, builder.mWidths.length); } } @LayoutlibDelegate /*package*/ static int nComputeLineBreaks(long nativeBuilder, LineBreaks recycle, int[] recycleBreaks, float[] recycleWidths, int[] recycleFlags, int recycleLength) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder == null) { return 0; } // compute all possible breakpoints. int length = builder.mWidths.length; BreakIterator it = BreakIterator.getLineInstance(new ULocale(builder.mLocales)); it.setText(new Segment(builder.mText, 0, length)); // average word length in english is 5. So, initialize the possible breaks with a guess. List breaks = new ArrayList((int) Math.ceil(length / 5d)); int loc; it.first(); while ((loc = it.next()) != BreakIterator.DONE) { breaks.add(loc); } List primitives = computePrimitives(builder.mText, builder.mWidths, length, breaks); switch (builder.mBreakStrategy) { case Layout.BREAK_STRATEGY_SIMPLE: builder.mLineBreaker = new GreedyLineBreaker(primitives, builder.mLineWidth, builder.mTabStopCalculator); break; case Layout.BREAK_STRATEGY_HIGH_QUALITY: // TODO // break; case Layout.BREAK_STRATEGY_BALANCED: builder.mLineBreaker = new OptimizingLineBreaker(primitives, builder.mLineWidth, builder.mTabStopCalculator); break; default: assert false : "Unknown break strategy: " + builder.mBreakStrategy; builder.mLineBreaker = new GreedyLineBreaker(primitives, builder.mLineWidth, builder.mTabStopCalculator); } builder.mLineBreaker.computeBreaks(recycle); return recycle.breaks.length; } /** * Compute metadata each character - things which help in deciding if it's possible to break * at a point or not. */ @NonNull private static List computePrimitives(@NonNull char[] text, @NonNull float[] widths, int length, @NonNull List breaks) { // Initialize the list with a guess of the number of primitives: // 2 Primitives per non-whitespace char and approx 5 chars per word (i.e. 83% chars) List primitives = new ArrayList(((int) Math.ceil(length * 1.833))); int breaksSize = breaks.size(); int breakIndex = 0; for (int i = 0; i < length; i++) { char c = text[i]; if (c == CHAR_SPACE || c == CHAR_ZWSP) { primitives.add(PrimitiveType.GLUE.getNewPrimitive(i, widths[i])); } else if (c == CHAR_TAB) { primitives.add(PrimitiveType.VARIABLE.getNewPrimitive(i)); } else if (c != CHAR_NEWLINE) { while (breakIndex < breaksSize && breaks.get(breakIndex) < i) { breakIndex++; } Primitive p; if (widths[i] != 0) { if (breakIndex < breaksSize && breaks.get(breakIndex) == i) { p = PrimitiveType.PENALTY.getNewPrimitive(i, 0, 0); } else { p = PrimitiveType.WORD_BREAK.getNewPrimitive(i, 0); } primitives.add(p); } primitives.add(PrimitiveType.BOX.getNewPrimitive(i, widths[i])); } } // final break at end of everything primitives.add( PrimitiveType.PENALTY.getNewPrimitive(length, 0, -PrimitiveType.PENALTY_INFINITY)); return primitives; } private static float measureText(long nativePaint, char []text, int index, int count, float[] widths, int bidiFlags) { Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint); RectF bounds = new BidiRenderer(null, paint, text) .renderText(index, index + count, bidiFlags, widths, 0, false); return bounds.right - bounds.left; } // TODO: Rename to LineBreakerRef and move everything other than LineBreaker to LineBreaker. /** * Java representation of the native Builder class. */ private static class Builder { String mLocales; char[] mText; float[] mWidths; LineBreaker mLineBreaker; long[] mNativeHyphenators; int mBreakStrategy; LineWidth mLineWidth; TabStops mTabStopCalculator; } }