1 /* 2 * Copyright (C) 2018 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.graphics.text; 18 19 import android.annotation.NonNull; 20 import android.graphics.text.Primitive.PrimitiveType; 21 22 import java.util.ArrayList; 23 import java.util.Collections; 24 import java.util.List; 25 import java.util.ListIterator; 26 27 import static android.graphics.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 BaseLineBreaker { 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()44 public Result computeBreaks() { 45 Result result = new Result(); 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 result.mLineBreakOffset.add(0); 53 result.mLineWidths.add(p.width); 54 result.mLineAscents.add(0f); 55 result.mLineDescents.add(0f); 56 result.mLineFlags.add(0); 57 return result; 58 } 59 Node[] opt = new Node[numBreaks]; 60 opt[0] = new Node(-1, 0, 0, 0, false); 61 opt[numBreaks - 1] = new Node(-1, 0, 0, 0, false); 62 63 ArrayList<Integer> active = new ArrayList<Integer>(); 64 active.add(0); 65 int lastBreak = 0; 66 for (int i = 0; i < numBreaks; i++) { 67 Primitive p = mPrimitives.get(i); 68 if (p.type == PrimitiveType.PENALTY) { 69 boolean finalBreak = (i + 1 == numBreaks); 70 Node bestBreak = null; 71 72 for (ListIterator<Integer> it = active.listIterator(); it.hasNext(); 73 /* incrementing done in loop */) { 74 int pos = it.next(); 75 int lines = opt[pos].mPrevCount; 76 float maxWidth = mLineWidth.getLineWidth(lines); 77 // we have to compute metrics every time -- 78 // we can't really pre-compute this stuff and just deal with breaks 79 // because of the way tab characters work, this makes it computationally 80 // harder, but this way, we can still optimize while treating tab characters 81 // correctly 82 LineMetrics lineMetrics = computeMetrics(pos, i); 83 if (lineMetrics.mPrintedWidth <= maxWidth) { 84 float demerits = computeDemerits(maxWidth, lineMetrics.mPrintedWidth, 85 finalBreak, p.penalty) + opt[pos].mDemerits; 86 if (bestBreak == null || demerits < bestBreak.mDemerits) { 87 if (bestBreak == null) { 88 bestBreak = new Node(pos, opt[pos].mPrevCount + 1, demerits, 89 lineMetrics.mPrintedWidth, lineMetrics.mHasTabs); 90 } else { 91 bestBreak.mPrev = pos; 92 bestBreak.mPrevCount = opt[pos].mPrevCount + 1; 93 bestBreak.mDemerits = demerits; 94 bestBreak.mWidth = lineMetrics.mPrintedWidth; 95 bestBreak.mHasTabs = lineMetrics.mHasTabs; 96 } 97 } 98 } else { 99 it.remove(); 100 } 101 } 102 if (p.penalty == -PENALTY_INFINITY) { 103 active.clear(); 104 } 105 if (bestBreak != null) { 106 opt[i] = bestBreak; 107 active.add(i); 108 lastBreak = i; 109 } 110 if (active.isEmpty()) { 111 // we can't give up! 112 LineMetrics lineMetrics = new LineMetrics(); 113 int lines = opt[lastBreak].mPrevCount; 114 float maxWidth = mLineWidth.getLineWidth(lines); 115 int breakIndex = desperateBreak(lastBreak, numBreaks, maxWidth, lineMetrics); 116 opt[breakIndex] = new Node(lastBreak, lines + 1, 0 /*doesn't matter*/, 117 lineMetrics.mWidth, lineMetrics.mHasTabs); 118 active.add(breakIndex); 119 lastBreak = breakIndex; 120 i = breakIndex; // incremented by i++ 121 } 122 } 123 } 124 125 int idx = numBreaks - 1; 126 while (opt[idx].mPrev != -1) { 127 result.mLineBreakOffset.add(mPrimitives.get(idx).location); 128 result.mLineWidths.add(opt[idx].mWidth); 129 result.mLineAscents.add(0f); 130 result.mLineDescents.add(0f); 131 result.mLineFlags.add(opt[idx].mHasTabs ? TAB_MASK : 0); 132 idx = opt[idx].mPrev; 133 } 134 135 Collections.reverse(result.mLineBreakOffset); 136 Collections.reverse(result.mLineWidths); 137 Collections.reverse(result.mLineAscents); 138 Collections.reverse(result.mLineDescents); 139 Collections.reverse(result.mLineFlags); 140 return result; 141 } 142 143 @NonNull computeMetrics(int start, int end)144 private LineMetrics computeMetrics(int start, int end) { 145 boolean f = false; 146 float w = 0, pw = 0; 147 for (int i = start; i < end; i++) { 148 Primitive p = mPrimitives.get(i); 149 if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) { 150 w += p.width; 151 if (p.type == PrimitiveType.BOX) { 152 pw = w; 153 } 154 } else if (p.type == PrimitiveType.VARIABLE) { 155 w = mTabStops.width(w); 156 f = true; 157 } 158 } 159 return new LineMetrics(w, pw, f); 160 } 161 computeDemerits(float maxWidth, float width, boolean finalBreak, float penalty)162 private static float computeDemerits(float maxWidth, float width, boolean finalBreak, 163 float penalty) { 164 float deviation = finalBreak ? 0 : maxWidth - width; 165 return (deviation * deviation) + penalty; 166 } 167 168 /** 169 * @return the last break position or -1 if failed. 170 */ 171 @SuppressWarnings("ConstantConditions") // method too complex to be analyzed. desperateBreak(int start, int limit, float maxWidth, @NonNull LineMetrics lineMetrics)172 private int desperateBreak(int start, int limit, float maxWidth, 173 @NonNull LineMetrics lineMetrics) { 174 float w = 0, pw = 0; 175 boolean breakFound = false; 176 int breakIndex = 0, firstTabIndex = Integer.MAX_VALUE; 177 for (int i = start; i < limit; i++) { 178 Primitive p = mPrimitives.get(i); 179 180 if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) { 181 w += p.width; 182 if (p.type == PrimitiveType.BOX) { 183 pw = w; 184 } 185 } else if (p.type == PrimitiveType.VARIABLE) { 186 w = mTabStops.width(w); 187 firstTabIndex = Math.min(firstTabIndex, i); 188 } 189 190 if (pw > maxWidth && breakFound) { 191 break; 192 } 193 194 // must make progress 195 if (i > start && 196 (p.type == PrimitiveType.PENALTY || p.type == PrimitiveType.WORD_BREAK)) { 197 breakFound = true; 198 breakIndex = i; 199 } 200 } 201 202 if (breakFound) { 203 lineMetrics.mWidth = w; 204 lineMetrics.mPrintedWidth = pw; 205 lineMetrics.mHasTabs = (start <= firstTabIndex && firstTabIndex < breakIndex); 206 return breakIndex; 207 } else { 208 return -1; 209 } 210 } 211 212 private static class LineMetrics { 213 /** Actual width of the line. */ 214 float mWidth; 215 /** Width of the line minus trailing whitespace. */ 216 float mPrintedWidth; 217 boolean mHasTabs; 218 LineMetrics()219 public LineMetrics() { 220 } 221 LineMetrics(float width, float printedWidth, boolean hasTabs)222 public LineMetrics(float width, float printedWidth, boolean hasTabs) { 223 mWidth = width; 224 mPrintedWidth = printedWidth; 225 mHasTabs = hasTabs; 226 } 227 } 228 229 /** 230 * A struct to store the info about a break. 231 */ 232 @SuppressWarnings("SpellCheckingInspection") // For the word struct. 233 private static class Node { 234 // -1 for the first node. 235 int mPrev; 236 // number of breaks so far. 237 int mPrevCount; 238 float mDemerits; 239 float mWidth; 240 boolean mHasTabs; 241 Node(int prev, int prevCount, float demerits, float width, boolean hasTabs)242 public Node(int prev, int prevCount, float demerits, float width, boolean hasTabs) { 243 mPrev = prev; 244 mPrevCount = prevCount; 245 mDemerits = demerits; 246 mWidth = width; 247 mHasTabs = hasTabs; 248 } 249 } 250 } 251