OptimizingLineBreaker.java revision 7053919a3a55ad1b58e6db701afda18850037732
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
19import com.android.annotations.NonNull;
20
21import android.text.Primitive.PrimitiveType;
22import android.text.StaticLayout.LineBreaks;
23
24import java.util.ArrayList;
25import java.util.List;
26import java.util.ListIterator;
27
28import static android.text.Primitive.PrimitiveType.PENALTY_INFINITY;
29
30
31// Based on the native implementation of OptimizingLineBreaker in
32// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260
33/**
34 * A more complex version of line breaking where we try to prevent the right edge from being too
35 * jagged.
36 */
37public class OptimizingLineBreaker extends LineBreaker {
38
39    public OptimizingLineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth,
40            @NonNull TabStops tabStops) {
41        super(primitives, lineWidth, tabStops);
42    }
43
44    @Override
45    public void computeBreaks(@NonNull LineBreaks breakInfo) {
46        int numBreaks = mPrimitives.size();
47        assert numBreaks > 0;
48        if (numBreaks == 1) {
49            // This can be true only if it's an empty paragraph.
50            Primitive p = mPrimitives.get(0);
51            assert p.type == PrimitiveType.PENALTY;
52            breakInfo.breaks = new int[]{0};
53            breakInfo.widths = new float[]{p.width};
54            breakInfo.flags = new boolean[]{false};
55            return;
56        }
57        Node[] opt = new Node[numBreaks];
58        opt[0] = new Node(-1, 0, 0, 0, false);
59        opt[numBreaks - 1] = new Node(-1, 0, 0, 0, false);
60
61        ArrayList<Integer> active = new ArrayList<Integer>();
62        active.add(0);
63        int lastBreak = 0;
64        for (int i = 0; i < numBreaks; i++) {
65            Primitive p = mPrimitives.get(i);
66            if (p.type == PrimitiveType.PENALTY) {
67                boolean finalBreak = (i + 1 == numBreaks);
68                Node bestBreak = null;
69
70                for (ListIterator<Integer> it = active.listIterator(); it.hasNext();
71                        /* incrementing done in loop */) {
72                    int pos = it.next();
73                    int lines = opt[pos].mPrevCount;
74                    float maxWidth = mLineWidth.getLineWidth(lines);
75                    // we have to compute metrics every time --
76                    // we can't really pre-compute this stuff and just deal with breaks
77                    // because of the way tab characters work, this makes it computationally
78                    // harder, but this way, we can still optimize while treating tab characters
79                    // correctly
80                    LineMetrics lineMetrics = computeMetrics(pos, i);
81                    if (lineMetrics.mPrintedWidth <= maxWidth) {
82                        float demerits = computeDemerits(maxWidth, lineMetrics.mPrintedWidth,
83                                finalBreak, p.penalty) + opt[pos].mDemerits;
84                        if (bestBreak == null || demerits < bestBreak.mDemerits) {
85                            if (bestBreak == null) {
86                                bestBreak = new Node(pos, opt[pos].mPrevCount + 1, demerits,
87                                        lineMetrics.mPrintedWidth, lineMetrics.mHasTabs);
88                            } else {
89                                bestBreak.mPrev = pos;
90                                bestBreak.mPrevCount = opt[pos].mPrevCount + 1;
91                                bestBreak.mDemerits = demerits;
92                                bestBreak.mWidth = lineMetrics.mPrintedWidth;
93                                bestBreak.mHasTabs = lineMetrics.mHasTabs;
94                            }
95                        }
96                    } else {
97                        it.remove();
98                    }
99                }
100                if (p.penalty == -PENALTY_INFINITY) {
101                    active.clear();
102                }
103                if (bestBreak != null) {
104                    opt[i] = bestBreak;
105                    active.add(i);
106                    lastBreak = i;
107                }
108                if (active.isEmpty()) {
109                    // we can't give up!
110                    LineMetrics lineMetrics = new LineMetrics();
111                    int lines = opt[lastBreak].mPrevCount;
112                    float maxWidth = mLineWidth.getLineWidth(lines);
113                    int breakIndex = desperateBreak(lastBreak, numBreaks, maxWidth, lineMetrics);
114                    opt[breakIndex] = new Node(lastBreak, lines + 1, 0 /*doesn't matter*/,
115                            lineMetrics.mWidth, lineMetrics.mHasTabs);
116                    active.add(breakIndex);
117                    lastBreak = breakIndex;
118                    i = breakIndex; // incremented by i++
119                }
120            }
121        }
122
123        int idx = numBreaks - 1;
124        int count = opt[idx].mPrevCount;
125        resize(breakInfo, count);
126        while (opt[idx].mPrev != -1) {
127            count--;
128            assert count >=0;
129
130            breakInfo.breaks[count] = mPrimitives.get(idx).location;
131            breakInfo.widths[count] = opt[idx].mWidth;
132            breakInfo.flags [count] = opt[idx].mHasTabs;
133            idx = opt[idx].mPrev;
134        }
135    }
136
137    private static void resize(LineBreaks lineBreaks, int size) {
138        if (lineBreaks.breaks.length == size) {
139            return;
140        }
141        int[] breaks = new int[size];
142        float[] widths = new float[size];
143        boolean[] flags = new boolean[size];
144
145        int toCopy = Math.min(size, lineBreaks.breaks.length);
146        System.arraycopy(lineBreaks.breaks, 0, breaks, 0, toCopy);
147        System.arraycopy(lineBreaks.widths, 0, widths, 0, toCopy);
148        System.arraycopy(lineBreaks.flags, 0, flags, 0, toCopy);
149
150        lineBreaks.breaks = breaks;
151        lineBreaks.widths = widths;
152        lineBreaks.flags = flags;
153    }
154
155    @NonNull
156    private LineMetrics computeMetrics(int start, int end) {
157        boolean f = false;
158        float w = 0, pw = 0;
159        for (int i = start; i < end; i++) {
160            Primitive p = mPrimitives.get(i);
161            if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) {
162                w += p.width;
163                if (p.type == PrimitiveType.BOX) {
164                    pw = w;
165                }
166            } else if (p.type == PrimitiveType.VARIABLE) {
167                w = mTabStops.width(w);
168                f = true;
169            }
170        }
171        return new LineMetrics(w, pw, f);
172    }
173
174    private static float computeDemerits(float maxWidth, float width, boolean finalBreak,
175            float penalty) {
176        float deviation = finalBreak ? 0 : maxWidth - width;
177        return (deviation * deviation) + penalty;
178    }
179
180    /**
181     * @return the last break position or -1 if failed.
182     */
183    @SuppressWarnings("ConstantConditions")  // method too complex to be analyzed.
184    private int desperateBreak(int start, int limit, float maxWidth,
185            @NonNull LineMetrics lineMetrics) {
186        float w = 0, pw = 0;
187        boolean breakFound = false;
188        int breakIndex = 0, firstTabIndex = Integer.MAX_VALUE;
189        for (int i = start; i < limit; i++) {
190            Primitive p = mPrimitives.get(i);
191
192            if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) {
193                w += p.width;
194                if (p.type == PrimitiveType.BOX) {
195                    pw = w;
196                }
197            } else if (p.type == PrimitiveType.VARIABLE) {
198                w = mTabStops.width(w);
199                firstTabIndex = Math.min(firstTabIndex, i);
200            }
201
202            if (pw > maxWidth && breakFound) {
203                break;
204            }
205
206            // must make progress
207            if (i > start &&
208                    (p.type == PrimitiveType.PENALTY || p.type == PrimitiveType.WORD_BREAK)) {
209                breakFound = true;
210                breakIndex = i;
211            }
212        }
213
214        if (breakFound) {
215            lineMetrics.mWidth = w;
216            lineMetrics.mPrintedWidth = pw;
217            lineMetrics.mHasTabs = (start <= firstTabIndex && firstTabIndex < breakIndex);
218            return breakIndex;
219        } else {
220            return -1;
221        }
222    }
223
224    private static class LineMetrics {
225        /** Actual width of the line. */
226        float mWidth;
227        /** Width of the line minus trailing whitespace. */
228        float mPrintedWidth;
229        boolean mHasTabs;
230
231        public LineMetrics() {
232        }
233
234        public LineMetrics(float width, float printedWidth, boolean hasTabs) {
235            mWidth = width;
236            mPrintedWidth = printedWidth;
237            mHasTabs = hasTabs;
238        }
239    }
240
241    /**
242     * A struct to store the info about a break.
243     */
244    @SuppressWarnings("SpellCheckingInspection")  // For the word struct.
245    private static class Node {
246        // -1 for the first node.
247        int mPrev;
248        // number of breaks so far.
249        int mPrevCount;
250        float mDemerits;
251        float mWidth;
252        boolean mHasTabs;
253
254        public Node(int prev, int prevCount, float demerits, float width, boolean hasTabs) {
255            mPrev = prev;
256            mPrevCount = prevCount;
257            mDemerits = demerits;
258            mWidth = width;
259            mHasTabs = hasTabs;
260        }
261    }
262}
263