/*
* 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.annotation.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(@NonNull 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 int[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 ? TAB_MASK : 0;
i++;
}
mBreaksList = null;
mWidthsList = null;
mFlagsList = null;
}
}
}