Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e8e45f2c authored by Doug Felt's avatar Doug Felt
Browse files

Refactor Styled utility functions into reusable objects.

This takes utility functions from Styled and a few other classes and
incorporates them into two new utility classes, TextLine and
MeasuredText.  The main point of this is to support shaping by skia,
to experiment with how this will look, this also introduces
character-based Arabic shaping.

MeasuredText is used by code that determines line breaks by generating
and examining character widths in logical order.  Factoring the code
in this way makes it usable by the ellipsize functions in TextUtils as
well as by StaticLayout.  This class takes over the caching of widths
and chars arrays that was previously performed by StyledText.  A small
number of MeasuredText objects are themselves cached by the class and
accesed using static obtain and recycle methods.  Generally only these
few cached instances are ever created.

TextLine is used by code that draws or measures text on a line.  This
unifies the line measuring and rendering code, and pushes assumptions
about how rtl text is treated closer to the points where skia code is
invoked.  TextLine implements the functions that were previously
provided by Styled, working on member arrays rather than
explicitly-passed arguments.  It implements the same kind of static
cache as MeasuredText.

TextLine and MeasureText simulate arabic glyph generation and shaping
by using ArabicShaping, ported with very minor changes from ICU4J's
ArabicShaping.  This class generates shaped Arabic glyphs and Lam-Alef
ligatures using Unicode presentation forms.  ArabicShaping is not
intended to be permanent, but to be replaced by real shaping from the
skia layer. It is introduced in order to emulate the behavior of real
shaping so that higher level code dealing with rendering shaped text
and cursor movement over ligatures can be developed and tested; it
also provides basic-level support for Arabic.

Since cursor movement depends on conjuncts whose formation is
font-dependent, cursor movement code that was formerly in Layout and
StaticLayout was moved into TextLine so that it can work on the shaped
text.

Other than these changes, the other major change is a rework of the
ellipsize utility functions to combine multiple branches into fewer
branches with additional state.

Updated copyright notices on new files.

Change-Id: I492cb58b51f5aaf6f14cb1419bdbed49eac5ba29
parent 5d470d03
Loading
Loading
Loading
Loading
+129 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.text;

import android.text.Layout.Directions;

/**
 * Access the ICU bidi implementation.
 * @hide
@@ -44,5 +46,132 @@ package android.text;
        return result;
    }

    /**
     * Returns run direction information for a line within a paragraph.
     *
     * @param dir base line direction, either Layout.DIR_LEFT_TO_RIGHT or
     *     Layout.DIR_RIGHT_TO_LEFT
     * @param levels levels as returned from {@link #bidi}
     * @param lstart start of the line in the levels array
     * @param chars the character array (used to determine whitespace)
     * @param cstart the start of the line in the chars array
     * @param len the length of the line
     * @return the directions
     */
    public static Directions directions(int dir, byte[] levels, int lstart,
            char[] chars, int cstart, int len) {

        int baseLevel = dir == Layout.DIR_LEFT_TO_RIGHT ? 0 : 1;
        int curLevel = levels[lstart];
        int minLevel = curLevel;
        int runCount = 1;
        for (int i = lstart + 1, e = lstart + len; i < e; ++i) {
            int level = levels[i];
            if (level != curLevel) {
                curLevel = level;
                ++runCount;
            }
        }

        // add final run for trailing counter-directional whitespace
        int visLen = len;
        if ((curLevel & 1) != (baseLevel & 1)) {
            // look for visible end
            while (--visLen >= 0) {
                char ch = chars[cstart + visLen];

                if (ch == '\n') {
                    --visLen;
                    break;
                }

                if (ch != ' ' && ch != '\t') {
                    break;
                }
            }
            ++visLen;
            if (visLen != len) {
                ++runCount;
            }
        }

        if (runCount == 1 && minLevel == baseLevel) {
            // we're done, only one run on this line
            if ((minLevel & 1) != 0) {
                return Layout.DIRS_ALL_RIGHT_TO_LEFT;
            }
            return Layout.DIRS_ALL_LEFT_TO_RIGHT;
        }

        int[] ld = new int[runCount * 2];
        int maxLevel = minLevel;
        int levelBits = minLevel << Layout.RUN_LEVEL_SHIFT;
        {
            // Start of first pair is always 0, we write
            // length then start at each new run, and the
            // last run length after we're done.
            int n = 1;
            int prev = lstart;
            curLevel = minLevel;
            for (int i = lstart, e = lstart + visLen; i < e; ++i) {
                int level = levels[i];
                if (level != curLevel) {
                    curLevel = level;
                    if (level > maxLevel) {
                        maxLevel = level;
                    } else if (level < minLevel) {
                        minLevel = level;
                    }
                    // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT
                    ld[n++] = (i - prev) | levelBits;
                    ld[n++] = i - lstart;
                    levelBits = curLevel << Layout.RUN_LEVEL_SHIFT;
                    prev = i;
                }
            }
            ld[n] = (lstart + visLen - prev) | levelBits;
            if (visLen < len) {
                ld[++n] = visLen;
                ld[++n] = (len - visLen) | (baseLevel << Layout.RUN_LEVEL_SHIFT);
            }
        }

        // See if we need to swap any runs.
        // If the min level run direction doesn't match the base
        // direction, we always need to swap (at this point
        // we have more than one run).
        // Otherwise, we don't need to swap the lowest level.
        // Since there are no logically adjacent runs at the same
        // level, if the max level is the same as the (new) min
        // level, we have a series of alternating levels that
        // is already in order, so there's no more to do.
        //
        boolean swap;
        if ((minLevel & 1) == baseLevel) {
            minLevel += 1;
            swap = maxLevel > minLevel;
        } else {
            swap = runCount > 1;
        }
        if (swap) {
            for (int level = maxLevel - 1; level >= minLevel; --level) {
                for (int i = 0; i < ld.length; i += 2) {
                    if (levels[ld[i]] >= level) {
                        int e = i + 2;
                        while (e < ld.length && levels[ld[e]] >= level) {
                            e += 2;
                        }
                        for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) {
                            int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x;
                            x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x;
                        }
                        i = e + 2;
                    }
                }
            }
        }
        return new Directions(ld);
    }

    private native static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo);
}
 No newline at end of file
+12 −13
Original line number Diff line number Diff line
@@ -208,11 +208,11 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback
             * width because the width that was passed in was for the
             * full text, not the ellipsized form.
             */
            synchronized (sTemp) {
                mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
                                                source, 0, source.length(),
                                                null)));
            }
            TextLine line = TextLine.obtain();
            line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
            mMax = (int) FloatMath.ceil(line.metrics(null));
            TextLine.recycle(line);
        }

        if (includepad) {
@@ -277,13 +277,12 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback
                fm = new Metrics();
            }

            int wid;
            TextLine line = TextLine.obtain();
            line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT,
                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
            fm.width = (int) FloatMath.ceil(line.metrics(fm));
            TextLine.recycle(line);

            synchronized (sTemp) {
                wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
                                                text, 0, text.length(), fm)));
            }
            fm.width = wid;
            return fm;
        } else {
            return null;
+82 −549

File changed.

Preview size limit exceeded, changes collapsed.

+250 −0
Original line number Diff line number Diff line
/*
 * 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.text;

import com.android.internal.util.ArrayUtils;

import android.graphics.Paint;
import android.icu.text.ArabicShaping;
import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
import android.util.Log;

/**
 * @hide
 */
class MeasuredText {
    /* package */ CharSequence mText;
    /* package */ int mTextStart;
    /* package */ float[] mWidths;
    /* package */ char[] mChars;
    /* package */ byte[] mLevels;
    /* package */ int mDir;
    /* package */ boolean mEasy;
    /* package */ int mLen;
    private int mPos;
    private float[] mWorkWidths; // temp buffer for Paint.measureText, arrgh
    private TextPaint mWorkPaint;

    private MeasuredText() {
        mWorkPaint = new TextPaint();
    }

    private static MeasuredText[] cached = new MeasuredText[3];

    /* package */
    static MeasuredText obtain() {
        MeasuredText mt;
        synchronized (cached) {
            for (int i = cached.length; --i >= 0;) {
                if (cached[i] != null) {
                    mt = cached[i];
                    cached[i] = null;
                    return mt;
                }
            }
        }
        mt = new MeasuredText();
        Log.e("MEAS", "new: " + mt);
        return mt;
    }

    /* package */
    static MeasuredText recycle(MeasuredText mt) {
        mt.mText = null;
        if (mt.mLen < 1000) {
            synchronized(cached) {
                for (int i = 0; i < cached.length; ++i) {
                    if (cached[i] == null) {
                        cached[i] = mt;
                        break;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Analyzes text for
     * bidirectional runs.  Allocates working buffers.
     */
    /* package */
    void setPara(CharSequence text, int start, int end, int bidiRequest) {
        mText = text;
        mTextStart = start;

        int len = end - start;
        mLen = len;
        mPos = 0;

        if (mWidths == null || mWidths.length < len) {
            mWidths = new float[ArrayUtils.idealFloatArraySize(len)];
            mWorkWidths = new float[mWidths.length];
        }
        if (mChars == null || mChars.length < len) {
            mChars = new char[ArrayUtils.idealCharArraySize(len)];
        }
        TextUtils.getChars(text, start, end, mChars, 0);

        if (text instanceof Spanned) {
            Spanned spanned = (Spanned) text;
            ReplacementSpan[] spans = spanned.getSpans(start, end,
                    ReplacementSpan.class);

            for (int i = 0; i < spans.length; i++) {
                int startInPara = spanned.getSpanStart(spans[i]) - start;
                int endInPara = spanned.getSpanEnd(spans[i]) - start;
                for (int j = startInPara; j < endInPara; j++) {
                    mChars[j] = '\uFFFC';
                }
            }
        }

        if (TextUtils.doesNotNeedBidi(mChars, 0, len)) {
            mDir = 1;
            mEasy = true;
        } else {
            if (mLevels == null || mLevels.length < len) {
                mLevels = new byte[ArrayUtils.idealByteArraySize(len)];
            }
            mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false);
            mEasy = false;

            // shape
            if (mLen > 0) {
                byte[] levels = mLevels;
                char[] chars = mChars;
                byte level = levels[0];
                int pi = 0;
                for (int i = 1, e = mLen;; ++i) {
                    if (i == e || levels[i] != level) {
                        if ((level & 0x1) != 0) {
                            AndroidCharacter.mirror(chars, pi, i - pi);
                            ArabicShaping.SHAPER.shape(chars, pi, i - pi);
                        }
                        if (i == e) {
                            break;
                        }
                        pi = i;
                        level = levels[i];
                    }
                }
            }
        }
    }

    float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
        int p = mPos;
        float[] w = mWidths, ww = mWorkWidths;
        int count = paint.getTextWidths(mChars, p, len, ww);
        int width = 0;
        if (count < len) {
            // must have surrogate pairs in here, pad out the array with zero
            // for the trailing surrogates
            char[] chars = mChars;
            for (int i = 0, e = mLen; i < count; ++i) {
                width += (w[p++] = ww[i]);
                if (p < e && chars[p] >= '\udc00' && chars[p] < '\ue000' &&
                        chars[p-1] >= '\ud800' && chars[p-1] < '\udc00') {
                    w[p++] = 0;
                }
            }
        } else {
            for (int i = 0; i < len; ++i) {
                width += (w[p++] = ww[i]);
            }
        }
        mPos = p;
        if (fm != null) {
            paint.getFontMetricsInt(fm);
        }
        return width;
    }

    float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
            Paint.FontMetricsInt fm) {

        TextPaint workPaint = mWorkPaint;
        workPaint.set(paint);
        // XXX paint should not have a baseline shift, but...
        workPaint.baselineShift = 0;

        ReplacementSpan replacement = null;
        for (int i = 0; i < spans.length; i++) {
            MetricAffectingSpan span = spans[i];
            if (span instanceof ReplacementSpan) {
                replacement = (ReplacementSpan)span;
            } else {
                span.updateMeasureState(workPaint);
            }
        }

        float wid;
        if (replacement == null) {
            wid = addStyleRun(workPaint, len, fm);
        } else {
            // 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 (fm != null) {
            if (workPaint.baselineShift < 0) {
                fm.ascent += workPaint.baselineShift;
                fm.top += workPaint.baselineShift;
            } else {
                fm.descent += workPaint.baselineShift;
                fm.bottom += workPaint.baselineShift;
            }
        }

        return wid;
    }

    int breakText(int start, int limit, boolean forwards, float width) {
        float[] w = mWidths;
        if (forwards) {
            for (int i = start; i < limit; ++i) {
                if ((width -= w[i]) < 0) {
                    return i - start;
                }
            }
        } else {
            for (int i = limit; --i >= start;) {
                if ((width -= w[i]) < 0) {
                    return limit - i -1;
                }
            }
        }

        return limit - start;
    }

    float measure(int start, int limit) {
        float width = 0;
        float[] w = mWidths;
        for (int i = start; i < limit; ++i) {
            width += w[i];
        }
        return width;
    }
}
 No newline at end of file
+92 −340

File changed.

Preview size limit exceeded, changes collapsed.

Loading