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
This commit is contained in:
Constantin Piber
2025-08-19 22:00:59 +03:00
committed by GitHub
parent 283e38c30a
commit b2cfb5a1e9
14 changed files with 221 additions and 29 deletions

View File

@@ -10,9 +10,11 @@ import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.TextAttribute;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;
@@ -44,13 +46,16 @@ public final class Canvas {
drawText(new String(text, index, count), x, y, paint);
}
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
public void drawText(@NonNull String str, float x, float y, @NonNull Paint paint) {
applyPaint(paint);
GlyphVector glyphVector = paint.getFont().createGlyphVector(canvas.getFontRenderContext(), text);
AttributedString text = paint.getTypeface().createWithFallback(str);
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
// TODO: fix with fallback fonts
GlyphVector glyphVector = paint.getTypeface().getFont().createGlyphVector(canvas.getFontRenderContext(), text.getIterator());
Shape textShape = glyphVector.getOutline();
switch (paint.getStyle()) {
case Paint.Style.FILL:
canvas.drawString(text, x, y);
canvas.drawString(text.getIterator(), x, y);
break;
case Paint.Style.STROKE:
save();
@@ -178,7 +183,7 @@ public final class Canvas {
}
private void applyPaint(Paint paint) {
canvas.setFont(paint.getFont());
canvas.setFont(paint.getTypeface().getFont());
java.awt.Color color = Color.valueOf(paint.getColorLong()).toJavaColor();
canvas.setColor(color);
canvas.setStroke(new BasicStroke(paint.getStrokeWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));

View File

@@ -65,9 +65,9 @@ public class Paint {
@ColorLong private long mShadowLayerColor;
private int mFlags;
private Font mFont = new Font(null);
private Style mStyle = Style.FILL;
private float mStrokeWidth = 1.0f;
private Typeface mTypeface = Typeface.DEFAULT;
private static final Object sCacheLock = new Object();
@@ -278,10 +278,10 @@ public class Paint {
mShadowLayerDy = 0.0f;
mShadowLayerColor = Color.pack(0);
setFlags(ANTI_ALIAS_FLAG);
mFont = new Font(null);
mStyle = Style.FILL;
mStrokeWidth = 1.0f;
mTypeface = Typeface.DEFAULT;
setFlags(ANTI_ALIAS_FLAG);
}
public void set(Paint src) {
@@ -314,9 +314,9 @@ public class Paint {
mShadowLayerColor = paint.mShadowLayerColor;
mFlags = paint.mFlags;
mFont = paint.mFont;
mStyle = paint.mStyle;
mStrokeWidth = paint.mStrokeWidth;
mTypeface = paint.mTypeface;
}
/** @hide */
@@ -368,13 +368,13 @@ public class Paint {
fontAttributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR);
}
Map<TextAttribute, Object> atts = (Map<TextAttribute, Object>) mFont.getAttributes();
Map<TextAttribute, Object> atts = mTypeface.getAttributes();
Object weight = atts.getOrDefault(TextAttribute.WEIGHT, null);
if (weight instanceof Float) {
fontAttributes.put(TextAttribute.WEIGHT, weight);
}
mFont = mFont.deriveFont(fontAttributes);
mTypeface = mTypeface.deriveFont(fontAttributes);
}
public int getHinting() {
@@ -605,14 +605,14 @@ public class Paint {
}
public Typeface getTypeface() {
return new Typeface(mFont);
return mTypeface;
}
public Typeface setTypeface(Typeface typeface) {
Map<TextAttribute, Object> fontAttributes = new HashMap<TextAttribute, Object>();
fontAttributes.put(TextAttribute.WEIGHT, typeface.getJavaWeight());
mFont = typeface.getFont()
.deriveFont(mFont.getStyle(), mFont.getSize())
mTypeface = typeface
.deriveFont(mTypeface.getFont().getStyle(), mTypeface.getFont().getSize())
.deriveFont(fontAttributes);
setFlags(mFlags);
return typeface;
@@ -693,16 +693,12 @@ public class Paint {
throw new RuntimeException("Stub!");
}
public Font getFont() {
return mFont;
}
public float getTextSize() {
return mFont.getSize2D();
return mTypeface.getFont().getSize2D();
}
public void setTextSize(float textSize) {
mFont = mFont.deriveFont(textSize);
mTypeface = mTypeface.deriveFont(textSize);
}
public float getTextScaleX() {
@@ -836,7 +832,7 @@ public class Paint {
public float getFontMetrics(FontMetrics metrics) {
java.awt.Canvas c = new java.awt.Canvas();
java.awt.FontMetrics m = c.getFontMetrics(mFont);
java.awt.FontMetrics m = c.getFontMetrics(mTypeface.getFont());
metrics.top = m.getMaxDescent();
metrics.ascent = m.getAscent();
metrics.descent = m.getDescent();

View File

@@ -30,9 +30,15 @@ import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import android.annotation.NonNull;
@@ -65,6 +71,7 @@ public class Typeface {
public static final String DEFAULT_FAMILY = "sans-serif";
private final Font mFont;
private final List<Font> mFallbackFonts;
/** Returns the typeface's weight value */
public int getWeight() {
@@ -271,6 +278,76 @@ public class Typeface {
public Typeface(Font fnt) {
mFont = fnt;
mFallbackFonts = Collections.emptyList();
}
public Typeface(Font fnt, List<Font> fallbackFonts) {
mFont = fnt;
mFallbackFonts = fallbackFonts;
}
public Map<TextAttribute, Object> getAttributes() {
return (Map<TextAttribute, Object>) mFont.getAttributes();
}
public Typeface deriveFont(Map<TextAttribute, Object> attributes) {
Font mainFont = mFont.deriveFont(attributes);
List<Font> fallbacks = mFallbackFonts.stream().map(font -> font.deriveFont(attributes))
.collect(Collectors.toList());
return new Typeface(mainFont, fallbacks);
}
public Typeface deriveFont(float size) {
Font mainFont = mFont.deriveFont(size);
List<Font> fallbacks = mFallbackFonts.stream().map(font -> font.deriveFont(size))
.collect(Collectors.toList());
return new Typeface(mainFont, fallbacks);
}
public Typeface deriveFont(int style, float size) {
Font mainFont = mFont.deriveFont(style, size);
List<Font> fallbacks = mFallbackFonts.stream().map(font -> font.deriveFont(style, size))
.collect(Collectors.toList());
return new Typeface(mainFont, fallbacks);
}
public AttributedString createWithFallback(String text) {
AttributedString result = new AttributedString(text);
int textLength = text.length();
result.addAttribute(TextAttribute.FONT, mFont, 0, textLength);
int i = 0;
while (true) {
int until = mFont.canDisplayUpTo(result.getIterator(), i, textLength);
if (until == -1) break;
boolean found = false;
// find a fallback font from `until`
for (int j = 0; j < mFallbackFonts.size(); ++j) {
int fallbackUntil = until;
for (; fallbackUntil < textLength; ++fallbackUntil) {
if (mFont.canDisplay(text.charAt(fallbackUntil)) || !mFallbackFonts.get(j).canDisplay(text.charAt(fallbackUntil)))
break;
}
if (fallbackUntil > until) {
// use this and advance
int end = fallbackUntil >= 0 ? fallbackUntil : textLength;
result.addAttribute(TextAttribute.FONT, mFallbackFonts.get(j), until, end);
Log.v(TAG, String.format("Fallback: from %d to %d using %s", until, end, mFallbackFonts.get(j).getName()));
i = end;
found = true;
break;
}
}
if (found) continue;
Log.w(TAG, String.format("No fallback font found at %d, skipping", until));
i = until + 1;
}
return result;
}
public static Typeface createFromFile(@Nullable File file) {
@@ -316,12 +393,30 @@ public class Typeface {
return mFont.hashCode();
}
private static Font loadFontAsset(String font) {
try (InputStream defaultNormalStream = ClassLoader.getSystemClassLoader().getResourceAsStream("font/" + font)) {
return Font.createFont(Font.TRUETYPE_FONT, defaultNormalStream).deriveFont(12.0f);
} catch (Exception ex) {
Log.e(TAG, "Failed to load " + font, ex);
return null;
}
}
private static Typeface withFallback(Font baseFallback, String mainFont, String... fonts) {
Font main = loadFontAsset(mainFont);
if (main == null) main = new Font(null, 0, 12);
List<Font> fallbacks = Stream.concat(Arrays.stream(fonts).map(Typeface::loadFontAsset).filter(f -> f != null), Stream.of(baseFallback))
.collect(Collectors.toList());
Log.v(TAG, String.format("Loaded font %s with %d fallback fonts", main.getName(), fallbacks.size()));
return new Typeface(main, fallbacks);
}
static {
DEFAULT = new Typeface(new Font(null, 0, 12));
DEFAULT_BOLD = new Typeface(new Font(null, Font.BOLD, 12));
SANS_SERIF = new Typeface(new Font(Font.SANS_SERIF, 0, 12));
SERIF = new Typeface(new Font(Font.SERIF, 0, 12));
MONOSPACE = new Typeface(new Font(Font.MONOSPACED, 0, 12));
DEFAULT = withFallback(new Font(null, 0, 12), "NotoSans/NotoSans-VariableFont_wdth,wght.ttf", "NotoSans/NotoSansSymbols2-Regular.ttf", "NotoSans/NotoEmoji-VariableFont_wght.ttf");
DEFAULT_BOLD = DEFAULT.deriveFont(Font.BOLD);
SANS_SERIF = DEFAULT;
SERIF = withFallback(new Font(Font.SERIF, 0, 12), "NotoSans/NotoSerif-VariableFont_wdth,wght.ttf", "NotoSans/NotoSansSymbols2-Regular.ttf", "NotoSans/NotoEmoji-VariableFont_wght.ttf");
MONOSPACE = withFallback(new Font(Font.MONOSPACED, 0, 12), "NotoSans/NotoSansMono-VariableFont_wdth,wght.ttf", "NotoSans/NotoSansSymbols2-Regular.ttf", "NotoSans/NotoEmoji-VariableFont_wght.ttf");
}
}

View File

@@ -366,9 +366,8 @@ public class StaticLayout extends Layout {
mRightIndents = b.mRightIndents;
String str = b.mText.subSequence(b.mStart, b.mEnd).toString();
AttributedString text = new AttributedString(str);
text.addAttribute(TextAttribute.FONT, getPaint().getFont());
FontRenderContext frc = new FontRenderContext(getPaint().getFont().getTransform(), RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
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

View File

@@ -27,6 +27,8 @@ import android.text.Layout.Directions;
import android.text.Layout.TabStops;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.TextMeasurer;
import java.text.AttributedString;
public class TextLine {
@@ -149,7 +151,9 @@ public class TextLine {
public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
@Nullable LineInfo lineInfo) {
FontRenderContext frc = new FontRenderContext(null, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
return (float) mPaint.getFont().getStringBounds(mText.toString(), mStart, mStart + mLen, frc).getWidth();
AttributedString text = mPaint.getTypeface().createWithFallback(mText.toString());
TextMeasurer tm = new TextMeasurer(text.getIterator(), frc);
return (float) tm.getLayout(mStart, mStart + mLen).getBounds().getWidth();
}
public float measure(@IntRange(from = 0) int offset, boolean trailing,