Files
Suwayomi-Server/AndroidCompat/src/main/java/android/text/StaticLayout.java
Constantin Piber b2cfb5a1e9 AndroidCompat: Use NotoSans as default font (#1572)
* Initial Noto fonts

* Use Noto for other default fonts

* Typeface: Prefer main font

Eagerly switch back to main font as soon as it can display again;
otherwise we might never switch back (or later than necessary); we
should always prefer the main font

* fix: Font metrics with fallback font on TextLine
2025-08-19 15:00:59 -04:00

631 lines
21 KiB
Java

/*
* Copyright (C) 2006 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.ColorInt;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.Log;
import java.awt.RenderingHints;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.text.AttributedString;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
public class StaticLayout extends Layout {
/*
* The break iteration is done in native code. The protocol for using the native code is as
* follows.
*
* First, call nInit to setup native line breaker object. Then, for each paragraph, do the
* following:
*
* - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in
* native.
* - Run LineBreaker.computeLineBreaks() to obtain line breaks for the paragraph.
*
* After all paragraphs, call finish() to release expensive buffers.
*/
static final String TAG = "StaticLayout";
public final static class Builder {
private Builder() {}
@NonNull
public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start,
@IntRange(from = 0) int end, @NonNull TextPaint paint,
@IntRange(from = 0) int width) {
Builder b = new Builder();
// set default initial values
b.mText = source;
b.mStart = start;
b.mEnd = end;
b.mPaint = paint;
b.mWidth = width;
b.mAlignment = Alignment.ALIGN_NORMAL;
b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
b.mIncludePad = true;
b.mFallbackLineSpacing = false;
b.mEllipsizedWidth = width;
b.mEllipsize = null;
b.mMaxLines = Integer.MAX_VALUE;
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
b.mMinimumFontMetrics = null;
return b;
}
// release any expensive state
/* package */ void finish() {
mText = null;
mPaint = null;
mLeftIndents = null;
mRightIndents = null;
mMinimumFontMetrics = null;
}
public Builder setText(CharSequence source) {
return setText(source, 0, source.length());
}
@NonNull
public Builder setText(@NonNull CharSequence source, int start, int end) {
mText = source;
mStart = start;
mEnd = end;
return this;
}
@NonNull
public Builder setPaint(@NonNull TextPaint paint) {
mPaint = paint;
return this;
}
@NonNull
public Builder setWidth(@IntRange(from = 0) int width) {
mWidth = width;
if (mEllipsize == null) {
mEllipsizedWidth = width;
}
return this;
}
@NonNull
public Builder setAlignment(@NonNull Alignment alignment) {
mAlignment = alignment;
return this;
}
@NonNull
public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
mTextDir = textDir;
return this;
}
@NonNull
public Builder setLineSpacing(float spacingAdd, float spacingMult) {
mSpacingAdd = spacingAdd;
mSpacingMult = spacingMult;
return this;
}
@NonNull
public Builder setIncludePad(boolean includePad) {
mIncludePad = includePad;
return this;
}
@NonNull
public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
mFallbackLineSpacing = useLineSpacingFromFallbacks;
return this;
}
@NonNull
public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
mEllipsizedWidth = ellipsizedWidth;
return this;
}
@NonNull
public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
mEllipsize = ellipsize;
return this;
}
@NonNull
public Builder setMaxLines(@IntRange(from = 0) int maxLines) {
mMaxLines = maxLines;
return this;
}
@NonNull
public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
mBreakStrategy = breakStrategy;
return this;
}
@NonNull
public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
mHyphenationFrequency = hyphenationFrequency;
return this;
}
@NonNull
public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) {
mLeftIndents = leftIndents;
mRightIndents = rightIndents;
return this;
}
@NonNull
public Builder setJustificationMode(@JustificationMode int justificationMode) {
mJustificationMode = justificationMode;
return this;
}
@NonNull
/* package */ Builder setAddLastLineLineSpacing(boolean value) {
mAddLastLineLineSpacing = value;
return this;
}
@SuppressLint("MissingGetterMatchingBuilder") // The base class `Layout` has a getter.
@NonNull
public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
mUseBoundsForWidth = useBoundsForWidth;
return this;
}
@NonNull
// The corresponding getter is getShiftDrawingOffsetForStartOverhang()
@SuppressLint("MissingGetterMatchingBuilder")
public Builder setShiftDrawingOffsetForStartOverhang(
boolean shiftDrawingOffsetForStartOverhang) {
mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
return this;
}
public Builder setCalculateBounds(boolean value) {
mCalculateBounds = value;
return this;
}
@NonNull
public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) {
mMinimumFontMetrics = minimumFontMetrics;
return this;
}
@NonNull
public StaticLayout build() {
StaticLayout result = new StaticLayout(this, mIncludePad, mEllipsize != null
? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
return result;
}
private CharSequence mText;
private int mStart;
private int mEnd;
private TextPaint mPaint;
private int mWidth;
private Alignment mAlignment;
private TextDirectionHeuristic mTextDir;
private float mSpacingMult;
private float mSpacingAdd;
private boolean mIncludePad;
private boolean mFallbackLineSpacing;
private int mEllipsizedWidth;
private TextUtils.TruncateAt mEllipsize;
private int mMaxLines;
private int mBreakStrategy;
private int mHyphenationFrequency;
@Nullable private int[] mLeftIndents;
@Nullable private int[] mRightIndents;
private int mJustificationMode;
private boolean mAddLastLineLineSpacing;
private boolean mUseBoundsForWidth;
private boolean mShiftDrawingOffsetForStartOverhang;
private boolean mCalculateBounds;
@Nullable private Paint.FontMetrics mMinimumFontMetrics;
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
}
private StaticLayout() {
super(
null, // text
null, // paint
0, // width
null, // alignment
null, // textDir
1, // spacing multiplier
0, // spacing amount
false, // include font padding
false, // fallback line spacing
0, // ellipsized width
null, // ellipsize
1, // maxLines
BREAK_STRATEGY_SIMPLE,
HYPHENATION_FREQUENCY_NONE,
null, // leftIndents
null, // rightIndents
JUSTIFICATION_MODE_NONE,
false, // useBoundsForWidth
false, // shiftDrawingOffsetForStartOverhang
null // minimumFontMetrics
);
mColumns = COLUMNS_ELLIPSIZE;
mLineDirections = new Directions[2];
mLines = new int[2 * mColumns];
}
@Deprecated
public StaticLayout(CharSequence source, TextPaint paint,
int width,
Alignment align, float spacingmult, float spacingadd,
boolean includepad) {
this(source, 0, source.length(), paint, width, align,
spacingmult, spacingadd, includepad);
}
@Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
boolean includepad) {
this(source, bufstart, bufend, paint, outerwidth, align,
spacingmult, spacingadd, includepad, null, 0);
}
@Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
this(source, bufstart, bufend, paint, outerwidth, align,
TextDirectionHeuristics.FIRSTSTRONG_LTR,
spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
}
@Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align, TextDirectionHeuristic textDir,
float spacingmult, float spacingadd,
boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
this(Builder.obtain(source, bufstart, bufend, paint, outerwidth)
.setAlignment(align)
.setTextDirection(textDir)
.setLineSpacing(spacingadd, spacingmult)
.setIncludePad(includepad)
.setEllipsize(ellipsize)
.setEllipsizedWidth(ellipsizedWidth)
.setMaxLines(maxLines), includepad,
ellipsize != null ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
}
private StaticLayout(Builder b, boolean trackPadding, int columnSize) {
super((b.mEllipsize == null) ? b.mText : (b.mText instanceof Spanned)
? new SpannedEllipsizer(b.mText) : new Ellipsizer(b.mText),
b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd,
b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize,
b.mMaxLines, b.mBreakStrategy, b.mHyphenationFrequency, b.mLeftIndents,
b.mRightIndents, b.mJustificationMode, b.mUseBoundsForWidth,
b.mShiftDrawingOffsetForStartOverhang, b.mMinimumFontMetrics);
mColumns = columnSize;
if (b.mEllipsize != null) {
Ellipsizer e = (Ellipsizer) getText();
e.mLayout = this;
e.mWidth = b.mEllipsizedWidth;
e.mMethod = b.mEllipsize;
throw new UnsupportedOperationException("Ellipsis not supported");
}
mLineDirections = new Directions[2];
mLines = new int[2 * mColumns];
mMaximumVisibleLineCount = b.mMaxLines;
mLeftIndents = b.mLeftIndents;
mRightIndents = b.mRightIndents;
String str = b.mText.subSequence(b.mStart, b.mEnd).toString();
AttributedString text = getPaint().getTypeface().createWithFallback(str);
FontRenderContext frc = new FontRenderContext(null, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
LineBreakMeasurer measurer = new LineBreakMeasurer(text.getIterator(), frc);
// TODO: directions
float y = 0;
while (measurer.getPosition() < str.length()) {
int off = mLineCount * mColumns;
int want = off + mColumns + TOP;
if (want >= mLines.length) {
final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
System.arraycopy(mLines, 0, grow, 0, mLines.length);
mLines = grow;
}
int pos = measurer.getPosition();
TextLayout l = measurer.nextLayout(getWidth());
mLines[off + START] = pos;
mLines[off + TOP] = (int) y;
mLines[off + DESCENT] = (int) (l.getDescent() + l.getLeading());
mLines[off + EXTRA] = (int) l.getLeading();
mLines[off + DIR] |= Layout.DIR_LEFT_TO_RIGHT << DIR_SHIFT;
y += l.getAscent();
y += l.getDescent() + l.getLeading();
mLines[off + mColumns + START] = measurer.getPosition();
mLines[off + mColumns + TOP] = (int) y;
mLineCount += 1;
}
}
// Override the base class so we can directly access our members,
// rather than relying on member functions.
// The logic mirrors that of Layout.getLineForVertical
// FIXME: It may be faster to do a linear search for layouts without many lines.
@Override
public int getLineForVertical(int vertical) {
int high = mLineCount;
int low = -1;
int guess;
int[] lines = mLines;
while (high - low > 1) {
guess = (high + low) >> 1;
if (lines[mColumns * guess + TOP] > vertical){
high = guess;
} else {
low = guess;
}
}
if (low < 0) {
return 0;
} else {
return low;
}
}
@Override
public int getLineCount() {
return mLineCount;
}
@Override
public int getLineTop(int line) {
return mLines[mColumns * line + TOP];
}
@Override
public int getLineExtra(int line) {
return mLines[mColumns * line + EXTRA];
}
@Override
public int getLineDescent(int line) {
return mLines[mColumns * line + DESCENT];
}
@Override
public int getLineStart(int line) {
return mLines[mColumns * line + START] & START_MASK;
}
@Override
public int getParagraphDirection(int line) {
return mLines[mColumns * line + DIR] >> DIR_SHIFT;
}
@Override
public boolean getLineContainsTab(int line) {
return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
}
@Override
public final Directions getLineDirections(int line) {
if (line > getLineCount()) {
throw new ArrayIndexOutOfBoundsException();
}
return new Directions(null);
// return mLineDirections[line];
}
@Override
public int getTopPadding() {
return mTopPadding;
}
@Override
public int getBottomPadding() {
return mBottomPadding;
}
// To store into single int field, pack the pair of start and end hyphen edit.
static int packHyphenEdit(
@Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) {
return start << START_HYPHEN_BITS_SHIFT | end;
}
static int unpackStartHyphenEdit(int packedHyphenEdit) {
return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT;
}
static int unpackEndHyphenEdit(int packedHyphenEdit) {
return packedHyphenEdit & END_HYPHEN_MASK;
}
@Override
public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) {
return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
}
@Override
public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) {
return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
}
@Override
public int getIndentAdjust(int line, Alignment align) {
if (align == Alignment.ALIGN_LEFT) {
if (mLeftIndents == null) {
return 0;
} else {
return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
}
} else if (align == Alignment.ALIGN_RIGHT) {
if (mRightIndents == null) {
return 0;
} else {
return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
}
} else if (align == Alignment.ALIGN_CENTER) {
int left = 0;
if (mLeftIndents != null) {
left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
}
int right = 0;
if (mRightIndents != null) {
right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
}
return (left - right) >> 1;
} else {
throw new AssertionError("unhandled alignment " + align);
}
}
@Override
public int getEllipsisCount(int line) {
if (mColumns < COLUMNS_ELLIPSIZE) {
return 0;
}
return mLines[mColumns * line + ELLIPSIS_COUNT];
}
@Override
public int getEllipsisStart(int line) {
if (mColumns < COLUMNS_ELLIPSIZE) {
return 0;
}
return mLines[mColumns * line + ELLIPSIS_START];
}
@Override
@NonNull
public RectF computeDrawingBoundingBox() {
// Cache the drawing bounds result because it does not change after created.
if (mDrawingBounds == null) {
mDrawingBounds = super.computeDrawingBoundingBox();
}
return mDrawingBounds;
}
@Override
public int getHeight(boolean cap) {
if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
&& Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "maxLineHeight should not be -1. "
+ " maxLines:" + mMaximumVisibleLineCount
+ " lineCount:" + mLineCount);
}
return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
? mMaxLineHeight : super.getHeight();
}
private int mLineCount;
private int mTopPadding, mBottomPadding;
private int mColumns;
private RectF mDrawingBounds = null; // lazy calculation.
private boolean mEllipsized;
private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
private static final int COLUMNS_NORMAL = 5;
private static final int COLUMNS_ELLIPSIZE = 7;
private static final int START = 0;
private static final int DIR = START;
private static final int TAB = START;
private static final int TOP = 1;
private static final int DESCENT = 2;
private static final int EXTRA = 3;
private static final int HYPHEN = 4;
private static final int ELLIPSIS_START = 5;
private static final int ELLIPSIS_COUNT = 6;
private int[] mLines;
private Directions[] mLineDirections;
private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
private static final int START_MASK = 0x1FFFFFFF;
private static final int DIR_SHIFT = 30;
private static final int TAB_MASK = 0x20000000;
private static final int HYPHEN_MASK = 0xFF;
private static final int START_HYPHEN_BITS_SHIFT = 3;
private static final int START_HYPHEN_MASK = 0x18; // 0b11000
private static final int END_HYPHEN_MASK = 0x7; // 0b00111
private static final float TAB_INCREMENT = 20; // same as Layout, but that's private
private static final char CHAR_NEW_LINE = '\n';
private static final double EXTRA_ROUNDING = 0.5;
private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
// Unused, here because of gray list private API accesses.
/*package*/ static class LineBreaks {
private static final int INITIAL_SIZE = 16;
public int[] breaks = new int[INITIAL_SIZE];
public float[] widths = new float[INITIAL_SIZE];
public float[] ascents = new float[INITIAL_SIZE];
public float[] descents = new float[INITIAL_SIZE];
public int[] flags = new int[INITIAL_SIZE]; // hasTab
// breaks, widths, and flags should all have the same length
}
@Nullable private int[] mLeftIndents;
@Nullable private int[] mRightIndents;
}