/*
* Copyright (C) 2018 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.graphics.text;
import com.android.layoutlib.bridge.impl.DelegateManager;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.icu.text.BreakIterator;
import android.text.Layout;
import android.text.Layout.BreakStrategy;
import android.text.Layout.HyphenationFrequency;
import android.graphics.text.Primitive.PrimitiveType;
import java.text.CharacterIterator;
import java.util.ArrayList;
import java.util.List;
import javax.swing.text.Segment;
import libcore.util.NativeAllocationRegistry_Delegate;
/**
* Delegate that provides implementation for native methods in {@link android.text.StaticLayout}
*
* Through the layoutlib_create tool, selected methods of StaticLayout have been replaced
* by calls to methods of the same name in this delegate class.
*
*/
public class LineBreaker_Delegate {
private static final char CHAR_SPACE = 0x20;
private static final char CHAR_TAB = 0x09;
private static final char CHAR_NEWLINE = 0x0A;
private static final char CHAR_ZWSP = 0x200B; // Zero width space.
// ---- Builder delegate manager ----
private static final DelegateManager sBuilderManager =
new DelegateManager<>(Builder.class);
private static long sFinalizer = -1;
// ---- Result delegate manager ----
private static final DelegateManager sResultManager =
new DelegateManager<>(Result.class);
private static long sResultFinalizer = -1;
@LayoutlibDelegate
/*package*/ static long nInit(
@BreakStrategy int breakStrategy,
@HyphenationFrequency int hyphenationFrequency,
boolean isJustified,
@Nullable int[] indents) {
Builder builder = new Builder();
builder.mBreakStrategy = breakStrategy;
return sBuilderManager.addNewDelegate(builder);
}
@LayoutlibDelegate
/*package*/ static long nGetReleaseFunc() {
synchronized (MeasuredText_Delegate.class) {
if (sFinalizer == -1) {
sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
sBuilderManager::removeJavaReferenceFor);
}
}
return sFinalizer;
}
@LayoutlibDelegate
/*package*/ static long nComputeLineBreaks(
/* non zero */ long nativePtr,
// Inputs
@NonNull char[] text,
long measuredTextPtr,
int length,
float firstWidth,
int firstWidthLineCount,
float restWidth,
@Nullable float[] variableTabStops,
float defaultTabStop,
int indentsOffset) {
Builder builder = sBuilderManager.getDelegate(nativePtr);
if (builder == null) {
return 0;
}
builder.mText = text;
builder.mWidths = new float[length];
builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth);
builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop);
MeasuredText_Delegate.computeRuns(measuredTextPtr, builder);
// compute all possible breakpoints.
BreakIterator it = BreakIterator.getLineInstance();
it.setText((CharacterIterator) new Segment(builder.mText, 0, length));
// average word length in english is 5. So, initialize the possible breaks with a guess.
List breaks = new ArrayList((int) Math.ceil(length / 5d));
int loc;
it.first();
while ((loc = it.next()) != BreakIterator.DONE) {
breaks.add(loc);
}
List primitives =
computePrimitives(builder.mText, builder.mWidths, length, breaks);
switch (builder.mBreakStrategy) {
case Layout.BREAK_STRATEGY_SIMPLE:
builder.mLineBreaker = new GreedyLineBreaker(primitives, builder.mLineWidth,
builder.mTabStopCalculator);
break;
case Layout.BREAK_STRATEGY_HIGH_QUALITY:
// TODO
// break;
case Layout.BREAK_STRATEGY_BALANCED:
builder.mLineBreaker = new OptimizingLineBreaker(primitives, builder.mLineWidth,
builder.mTabStopCalculator);
break;
default:
assert false : "Unknown break strategy: " + builder.mBreakStrategy;
builder.mLineBreaker = new GreedyLineBreaker(primitives, builder.mLineWidth,
builder.mTabStopCalculator);
}
Result result = new Result(builder.mLineBreaker.computeBreaks());
return sResultManager.addNewDelegate(result);
}
@LayoutlibDelegate
/*package*/ static int nGetLineCount(long ptr) {
Result result = sResultManager.getDelegate(ptr);
return result.mResult.mLineBreakOffset.size();
}
@LayoutlibDelegate
/*package*/ static int nGetLineBreakOffset(long ptr, int idx) {
Result result = sResultManager.getDelegate(ptr);
return result.mResult.mLineBreakOffset.get(idx);
}
@LayoutlibDelegate
/*package*/ static float nGetLineWidth(long ptr, int idx) {
Result result = sResultManager.getDelegate(ptr);
return result.mResult.mLineWidths.get(idx);
}
@LayoutlibDelegate
/*package*/ static float nGetLineAscent(long ptr, int idx) {
Result result = sResultManager.getDelegate(ptr);
return result.mResult.mLineAscents.get(idx);
}
@LayoutlibDelegate
/*package*/ static float nGetLineDescent(long ptr, int idx) {
Result result = sResultManager.getDelegate(ptr);
return result.mResult.mLineDescents.get(idx);
}
@LayoutlibDelegate
/*package*/ static int nGetLineFlag(long ptr, int idx) {
Result result = sResultManager.getDelegate(ptr);
return result.mResult.mLineFlags.get(idx);
}
@LayoutlibDelegate
/*package*/ static long nGetReleaseResultFunc() {
synchronized (MeasuredText_Delegate.class) {
if (sResultFinalizer == -1) {
sResultFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
sBuilderManager::removeJavaReferenceFor);
}
}
return sResultFinalizer;
}
/**
* Compute metadata each character - things which help in deciding if it's possible to break
* at a point or not.
*/
@NonNull
private static List computePrimitives(@NonNull char[] text, @NonNull float[] widths,
int length, @NonNull List breaks) {
// Initialize the list with a guess of the number of primitives:
// 2 Primitives per non-whitespace char and approx 5 chars per word (i.e. 83% chars)
List primitives = new ArrayList(((int) Math.ceil(length * 1.833)));
int breaksSize = breaks.size();
int breakIndex = 0;
for (int i = 0; i < length; i++) {
char c = text[i];
if (c == CHAR_SPACE || c == CHAR_ZWSP) {
primitives.add(PrimitiveType.GLUE.getNewPrimitive(i, widths[i]));
} else if (c == CHAR_TAB) {
primitives.add(PrimitiveType.VARIABLE.getNewPrimitive(i));
} else if (c != CHAR_NEWLINE) {
while (breakIndex < breaksSize && breaks.get(breakIndex) < i) {
breakIndex++;
}
Primitive p;
if (widths[i] != 0) {
if (breakIndex < breaksSize && breaks.get(breakIndex) == i) {
p = PrimitiveType.PENALTY.getNewPrimitive(i, 0, 0);
} else {
p = PrimitiveType.WORD_BREAK.getNewPrimitive(i, 0);
}
primitives.add(p);
}
primitives.add(PrimitiveType.BOX.getNewPrimitive(i, widths[i]));
}
}
// final break at end of everything
primitives.add(
PrimitiveType.PENALTY.getNewPrimitive(length, 0, -PrimitiveType.PENALTY_INFINITY));
return primitives;
}
// TODO: Rename to LineBreakerRef and move everything other than LineBreaker to LineBreaker.
/**
* Java representation of the native Builder class.
*/
public static class Builder {
char[] mText;
float[] mWidths;
private BaseLineBreaker mLineBreaker;
private int mBreakStrategy;
private LineWidth mLineWidth;
private TabStops mTabStopCalculator;
}
public abstract static class Run {
int mStart;
int mEnd;
Run(int start, int end) {
mStart = start;
mEnd = end;
}
abstract void addTo(Builder builder);
}
public static class Result {
final BaseLineBreaker.Result mResult;
public Result(BaseLineBreaker.Result result) {
mResult = result;
}
}
}