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