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

Commit eea62181 authored by Deepanshu Gupta's avatar Deepanshu Gupta Committed by Android (Google) Code Review
Browse files

Merge "Add Optimized Line breaking to LayoutLib"

parents 2e594f1e 7053919a
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -803,7 +803,7 @@ public class StaticLayout extends Layout {

    // This is used to return three arrays from a single JNI call when
    // performing line breaking
    private static class LineBreaks {
    /*package*/ static class LineBreaks {
        private static final int INITIAL_SIZE = 16;
        public int[] breaks = new int[INITIAL_SIZE];
        public float[] widths = new float[INITIAL_SIZE];
+193 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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.annotations.NonNull;

import android.text.Primitive.PrimitiveType;
import android.text.StaticLayout.LineBreaks;

import java.util.ArrayList;
import java.util.List;

import static android.text.Primitive.PrimitiveType.PENALTY_INFINITY;

// Based on the native implementation of GreedyLineBreaker in
// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260
public class GreedyLineBreaker extends LineBreaker {

    public GreedyLineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth,
            @NonNull TabStops tabStops) {
        super(primitives, lineWidth, tabStops);
    }

    @Override
    public void computeBreaks(LineBreaks lineBreaks) {
        BreakInfo breakInfo = new BreakInfo();
        int lineNum = 0;
        float width = 0, printedWidth = 0;
        boolean breakFound = false, goodBreakFound = false;
        int breakIndex = 0, goodBreakIndex = 0;
        float breakWidth = 0, goodBreakWidth = 0;
        int firstTabIndex = Integer.MAX_VALUE;

        float maxWidth = mLineWidth.getLineWidth(lineNum);

        int numPrimitives = mPrimitives.size();
        // greedily fit as many characters as possible on each line
        // loop over all primitives, and choose the best break point
        // (if possible, a break point without splitting a word)
        // after going over the maximum length
        for (int i = 0; i < numPrimitives; i++) {
            Primitive p = mPrimitives.get(i);

            // update the current line width
            if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) {
                width += p.width;
                if (p.type == PrimitiveType.BOX) {
                    printedWidth = width;
                }
            } else if (p.type == PrimitiveType.VARIABLE) {
                width = mTabStops.width(width);
                // keep track of first tab character in the region we are examining
                // so we can determine whether or not a line contains a tab
                firstTabIndex = Math.min(firstTabIndex, i);
            }

            // find the best break point for the characters examined so far
            if (printedWidth > maxWidth) {
                //noinspection StatementWithEmptyBody
                if (breakFound || goodBreakFound) {
                    if (goodBreakFound) {
                        // a true line break opportunity existed in the characters examined so far,
                        // so there is no need to split a word
                        i = goodBreakIndex; // no +1 because of i++
                        lineNum++;
                        maxWidth = mLineWidth.getLineWidth(lineNum);
                        breakInfo.mBreaksList.add(mPrimitives.get(goodBreakIndex).location);
                        breakInfo.mWidthsList.add(goodBreakWidth);
                        breakInfo.mFlagsList.add(firstTabIndex < goodBreakIndex);
                        firstTabIndex = Integer.MAX_VALUE;
                    } else {
                        // must split a word because there is no other option
                        i = breakIndex; // no +1 because of i++
                        lineNum++;
                        maxWidth = mLineWidth.getLineWidth(lineNum);
                        breakInfo.mBreaksList.add(mPrimitives.get(breakIndex).location);
                        breakInfo.mWidthsList.add(breakWidth);
                        breakInfo.mFlagsList.add(firstTabIndex < breakIndex);
                        firstTabIndex = Integer.MAX_VALUE;
                    }
                    printedWidth = width = 0;
                    goodBreakFound = breakFound = false;
                    goodBreakWidth = breakWidth = 0;
                    continue;
                } else {
                    // no choice, keep going... must make progress by putting at least one
                    // character on a line, even if part of that character is cut off --
                    // there is no other option
                }
            }

            // update possible break points
            if (p.type == PrimitiveType.PENALTY &&
                    p.penalty < PENALTY_INFINITY) {
                // this does not handle penalties with width

                // handle forced line break
                if (p.penalty == -PENALTY_INFINITY) {
                    lineNum++;
                    maxWidth = mLineWidth.getLineWidth(lineNum);
                    breakInfo.mBreaksList.add(p.location);
                    breakInfo.mWidthsList.add(printedWidth);
                    breakInfo.mFlagsList.add(firstTabIndex < i);
                    firstTabIndex = Integer.MAX_VALUE;
                    printedWidth = width = 0;
                    goodBreakFound = breakFound = false;
                    goodBreakWidth = breakWidth = 0;
                    continue;
                }
                if (i > breakIndex && (printedWidth <= maxWidth || !breakFound)) {
                    breakFound = true;
                    breakIndex = i;
                    breakWidth = printedWidth;
                }
                if (i > goodBreakIndex && printedWidth <= maxWidth) {
                    goodBreakFound = true;
                    goodBreakIndex = i;
                    goodBreakWidth = printedWidth;
                }
            } else if (p.type == PrimitiveType.WORD_BREAK) {
                // only do this if necessary -- we don't want to break words
                // when possible, but sometimes it is unavoidable
                if (i > breakIndex && (printedWidth <= maxWidth || !breakFound)) {
                    breakFound = true;
                    breakIndex = i;
                    breakWidth = printedWidth;
                }
            }
        }

        if (breakFound || goodBreakFound) {
            // output last break if there are more characters to output
            if (goodBreakFound) {
                breakInfo.mBreaksList.add(mPrimitives.get(goodBreakIndex).location);
                breakInfo.mWidthsList.add(goodBreakWidth);
                breakInfo.mFlagsList.add(firstTabIndex < goodBreakIndex);
            } else {
                breakInfo.mBreaksList.add(mPrimitives.get(breakIndex).location);
                breakInfo.mWidthsList.add(breakWidth);
                breakInfo.mFlagsList.add(firstTabIndex < breakIndex);
            }
        }
        breakInfo.copyTo(lineBreaks);
    }

    private static class BreakInfo {
        List<Integer> mBreaksList = new ArrayList<Integer>();
        List<Float> mWidthsList = new ArrayList<Float>();
        List<Boolean> mFlagsList = new ArrayList<Boolean>();

        public void copyTo(LineBreaks lineBreaks) {
            if (lineBreaks.breaks.length != mBreaksList.size()) {
                lineBreaks.breaks = new int[mBreaksList.size()];
                lineBreaks.widths = new float[mWidthsList.size()];
                lineBreaks.flags = new boolean[mFlagsList.size()];
            }

            int i = 0;
            for (int b : mBreaksList) {
                lineBreaks.breaks[i] = b;
                i++;
            }
            i = 0;
            for (float b : mWidthsList) {
                lineBreaks.widths[i] = b;
                i++;
            }
            i = 0;
            for (boolean b : mFlagsList) {
                lineBreaks.flags[i] = b;
                i++;
            }

            mBreaksList = null;
            mWidthsList = null;
            mFlagsList = null;
        }
    }
}
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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 android.text.StaticLayout.LineBreaks;
import com.android.annotations.NonNull;

import java.util.Collections;
import java.util.List;

// Based on the native implementation of LineBreaker in
// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260
public abstract class LineBreaker {

    protected final @NonNull List<Primitive> mPrimitives;
    protected final @NonNull LineWidth mLineWidth;
    protected final @NonNull TabStops mTabStops;

    public LineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth,
            @NonNull TabStops tabStops) {
        mPrimitives = Collections.unmodifiableList(primitives);
        mLineWidth = lineWidth;
        mTabStops = tabStops;
    }

    @NonNull
    public abstract void computeBreaks(@NonNull LineBreaks breakInfo);
}
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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;

// Based on the native implementation of LineWidth in
// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260
public class LineWidth {
    private final float mFirstWidth;
    private final int mFirstWidthLineCount;
    private float mRestWidth;

    public LineWidth(float firstWidth, int firstWidthLineCount, float restWidth) {
        mFirstWidth = firstWidth;
        mFirstWidthLineCount = firstWidthLineCount;
        mRestWidth = restWidth;
    }

    public float getLineWidth(int line) {
        return (line < mFirstWidthLineCount) ? mFirstWidth : mRestWidth;
    }
}
+262 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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.annotations.NonNull;

import android.text.Primitive.PrimitiveType;
import android.text.StaticLayout.LineBreaks;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import static android.text.Primitive.PrimitiveType.PENALTY_INFINITY;


// Based on the native implementation of OptimizingLineBreaker in
// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260
/**
 * A more complex version of line breaking where we try to prevent the right edge from being too
 * jagged.
 */
public class OptimizingLineBreaker extends LineBreaker {

    public OptimizingLineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth,
            @NonNull TabStops tabStops) {
        super(primitives, lineWidth, tabStops);
    }

    @Override
    public void computeBreaks(@NonNull LineBreaks breakInfo) {
        int numBreaks = mPrimitives.size();
        assert numBreaks > 0;
        if (numBreaks == 1) {
            // This can be true only if it's an empty paragraph.
            Primitive p = mPrimitives.get(0);
            assert p.type == PrimitiveType.PENALTY;
            breakInfo.breaks = new int[]{0};
            breakInfo.widths = new float[]{p.width};
            breakInfo.flags = new boolean[]{false};
            return;
        }
        Node[] opt = new Node[numBreaks];
        opt[0] = new Node(-1, 0, 0, 0, false);
        opt[numBreaks - 1] = new Node(-1, 0, 0, 0, false);

        ArrayList<Integer> active = new ArrayList<Integer>();
        active.add(0);
        int lastBreak = 0;
        for (int i = 0; i < numBreaks; i++) {
            Primitive p = mPrimitives.get(i);
            if (p.type == PrimitiveType.PENALTY) {
                boolean finalBreak = (i + 1 == numBreaks);
                Node bestBreak = null;

                for (ListIterator<Integer> it = active.listIterator(); it.hasNext();
                        /* incrementing done in loop */) {
                    int pos = it.next();
                    int lines = opt[pos].mPrevCount;
                    float maxWidth = mLineWidth.getLineWidth(lines);
                    // we have to compute metrics every time --
                    // we can't really pre-compute this stuff and just deal with breaks
                    // because of the way tab characters work, this makes it computationally
                    // harder, but this way, we can still optimize while treating tab characters
                    // correctly
                    LineMetrics lineMetrics = computeMetrics(pos, i);
                    if (lineMetrics.mPrintedWidth <= maxWidth) {
                        float demerits = computeDemerits(maxWidth, lineMetrics.mPrintedWidth,
                                finalBreak, p.penalty) + opt[pos].mDemerits;
                        if (bestBreak == null || demerits < bestBreak.mDemerits) {
                            if (bestBreak == null) {
                                bestBreak = new Node(pos, opt[pos].mPrevCount + 1, demerits,
                                        lineMetrics.mPrintedWidth, lineMetrics.mHasTabs);
                            } else {
                                bestBreak.mPrev = pos;
                                bestBreak.mPrevCount = opt[pos].mPrevCount + 1;
                                bestBreak.mDemerits = demerits;
                                bestBreak.mWidth = lineMetrics.mPrintedWidth;
                                bestBreak.mHasTabs = lineMetrics.mHasTabs;
                            }
                        }
                    } else {
                        it.remove();
                    }
                }
                if (p.penalty == -PENALTY_INFINITY) {
                    active.clear();
                }
                if (bestBreak != null) {
                    opt[i] = bestBreak;
                    active.add(i);
                    lastBreak = i;
                }
                if (active.isEmpty()) {
                    // we can't give up!
                    LineMetrics lineMetrics = new LineMetrics();
                    int lines = opt[lastBreak].mPrevCount;
                    float maxWidth = mLineWidth.getLineWidth(lines);
                    int breakIndex = desperateBreak(lastBreak, numBreaks, maxWidth, lineMetrics);
                    opt[breakIndex] = new Node(lastBreak, lines + 1, 0 /*doesn't matter*/,
                            lineMetrics.mWidth, lineMetrics.mHasTabs);
                    active.add(breakIndex);
                    lastBreak = breakIndex;
                    i = breakIndex; // incremented by i++
                }
            }
        }

        int idx = numBreaks - 1;
        int count = opt[idx].mPrevCount;
        resize(breakInfo, count);
        while (opt[idx].mPrev != -1) {
            count--;
            assert count >=0;

            breakInfo.breaks[count] = mPrimitives.get(idx).location;
            breakInfo.widths[count] = opt[idx].mWidth;
            breakInfo.flags [count] = opt[idx].mHasTabs;
            idx = opt[idx].mPrev;
        }
    }

    private static void resize(LineBreaks lineBreaks, int size) {
        if (lineBreaks.breaks.length == size) {
            return;
        }
        int[] breaks = new int[size];
        float[] widths = new float[size];
        boolean[] flags = new boolean[size];

        int toCopy = Math.min(size, lineBreaks.breaks.length);
        System.arraycopy(lineBreaks.breaks, 0, breaks, 0, toCopy);
        System.arraycopy(lineBreaks.widths, 0, widths, 0, toCopy);
        System.arraycopy(lineBreaks.flags, 0, flags, 0, toCopy);

        lineBreaks.breaks = breaks;
        lineBreaks.widths = widths;
        lineBreaks.flags = flags;
    }

    @NonNull
    private LineMetrics computeMetrics(int start, int end) {
        boolean f = false;
        float w = 0, pw = 0;
        for (int i = start; i < end; i++) {
            Primitive p = mPrimitives.get(i);
            if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) {
                w += p.width;
                if (p.type == PrimitiveType.BOX) {
                    pw = w;
                }
            } else if (p.type == PrimitiveType.VARIABLE) {
                w = mTabStops.width(w);
                f = true;
            }
        }
        return new LineMetrics(w, pw, f);
    }

    private static float computeDemerits(float maxWidth, float width, boolean finalBreak,
            float penalty) {
        float deviation = finalBreak ? 0 : maxWidth - width;
        return (deviation * deviation) + penalty;
    }

    /**
     * @return the last break position or -1 if failed.
     */
    @SuppressWarnings("ConstantConditions")  // method too complex to be analyzed.
    private int desperateBreak(int start, int limit, float maxWidth,
            @NonNull LineMetrics lineMetrics) {
        float w = 0, pw = 0;
        boolean breakFound = false;
        int breakIndex = 0, firstTabIndex = Integer.MAX_VALUE;
        for (int i = start; i < limit; i++) {
            Primitive p = mPrimitives.get(i);

            if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) {
                w += p.width;
                if (p.type == PrimitiveType.BOX) {
                    pw = w;
                }
            } else if (p.type == PrimitiveType.VARIABLE) {
                w = mTabStops.width(w);
                firstTabIndex = Math.min(firstTabIndex, i);
            }

            if (pw > maxWidth && breakFound) {
                break;
            }

            // must make progress
            if (i > start &&
                    (p.type == PrimitiveType.PENALTY || p.type == PrimitiveType.WORD_BREAK)) {
                breakFound = true;
                breakIndex = i;
            }
        }

        if (breakFound) {
            lineMetrics.mWidth = w;
            lineMetrics.mPrintedWidth = pw;
            lineMetrics.mHasTabs = (start <= firstTabIndex && firstTabIndex < breakIndex);
            return breakIndex;
        } else {
            return -1;
        }
    }

    private static class LineMetrics {
        /** Actual width of the line. */
        float mWidth;
        /** Width of the line minus trailing whitespace. */
        float mPrintedWidth;
        boolean mHasTabs;

        public LineMetrics() {
        }

        public LineMetrics(float width, float printedWidth, boolean hasTabs) {
            mWidth = width;
            mPrintedWidth = printedWidth;
            mHasTabs = hasTabs;
        }
    }

    /**
     * A struct to store the info about a break.
     */
    @SuppressWarnings("SpellCheckingInspection")  // For the word struct.
    private static class Node {
        // -1 for the first node.
        int mPrev;
        // number of breaks so far.
        int mPrevCount;
        float mDemerits;
        float mWidth;
        boolean mHasTabs;

        public Node(int prev, int prevCount, float demerits, float width, boolean hasTabs) {
            mPrev = prev;
            mPrevCount = prevCount;
            mDemerits = demerits;
            mWidth = width;
            mHasTabs = hasTabs;
        }
    }
}
Loading