Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2/n Nested Text textAlignVertical (Android) - adding textAlignVertical prop to NestedText #35704

Closed
wants to merge 65 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
c683779
draft example to add superscript to Text
fabOnReact Dec 22, 2022
6716e0f
cpp TextAttributes settings crash RNTester
fabOnReact Dec 22, 2022
7d147de
passing textAlignVert from js NestedText to Java
fabOnReact Dec 22, 2022
7914cfd
Verify configs behind mIsAccessibilityLink or check corresponding con…
fabOnReact Dec 26, 2022
fdeb37c
Modify Superscript implementation to align to the top (stackoverflow-…
fabOnReact Dec 26, 2022
5565bc1
adapt Superscript to top/bottom
fabOnReact Dec 27, 2022
9de465b
Update TopAlignSpan baseline position from ReactTextView#onLayout wit…
fabOnReact Dec 28, 2022
7ab97c2
Add logic to detect parent textAlignVertical and correctly shift base…
fabOnReact Dec 28, 2022
0f7d69d
delete ReactBottomAlignSpan
fabOnReact Dec 28, 2022
59fe775
Avoid adding ReactAlignSpan to span of the parent TextView
fabOnReact Dec 29, 2022
5017156
The calculatedHeight used to compute the margin is an aproximate valu…
fabOnReact Jan 2, 2023
f75e899
Nested Text correctly aligned multiline spans (different positions an…
fabOnReact Jan 2, 2023
ba18167
refactor algo
fabOnReact Jan 2, 2023
9a7ad5b
Text correctly aligns with nested inline image. The height of the inl…
fabOnReact Jan 4, 2023
61e5089
Use lineHeight to position span textAlignVertical
fabOnReact Jan 5, 2023
582faa4
Parent component does not have align property
fabOnReact Jan 5, 2023
8573fd5
adding different test scenarios
fabOnReact Jan 5, 2023
28608f1
Bottom and center alignment
fabOnReact Jan 5, 2023
957c784
minor changes
fabOnReact Jan 5, 2023
ba5223b
Changing text align from center to top results in mHighestLineHeight …
fabOnReact Jan 10, 2023
47ddd11
Text does not align correctly without lineHeight prop (video, stackov…
fabOnReact Jan 10, 2023
ce2b8eb
fix bottom alignment when lineHeight prop provided
fabOnReact Jan 10, 2023
87d205f
remove not relevant code
fabOnReact Jan 10, 2023
7ac7a9a
move ReactAlignSpan to CustomStyleSpan
fabOnReact Jan 11, 2023
bec2e1d
adding comments to baselineShift logic
fabOnReact Jan 11, 2023
2ff855b
update comments
fabOnReact Jan 11, 2023
ee66a98
reapply relevant changes from PR#7
fabOnReact Jan 11, 2023
b3efb82
Merge branch 'main' into nested-text-superscript
fabOnReact Jan 12, 2023
395bbd2
Move ReactAbsSizeSpan to separate PR and organize task in separate table
fabOnReact Jan 12, 2023
af8a7e9
Calculate font top and bottom with different fontSizes
fabOnReact Jan 13, 2023
e363196
avoid using TextBounds in AbsoluteSpan
fabOnReact Jan 13, 2023
dbe25d3
adding constructor to CustomStyleSpan and AbsoluteSpan
fabOnReact Jan 13, 2023
812d406
Combining lineHeight and fontSize can cause not correct top/bottom al…
fabOnReact Jan 13, 2023
4f6095e
Initial position is not correct (screenshot). 1 line is added to base…
fabOnReact Jan 13, 2023
49abefd
Avoid removing ascent or descent when lineHeight is higher then fontM…
fabOnReact Jan 16, 2023
469c6d7
Fixing issue with ascent and descent when aligning text with lineHeight
fabOnReact Jan 16, 2023
003ab18
Fix flow errors (link1, link2, link3)
fabOnReact Jan 17, 2023
17c70ad
Fix flow errors (link1, link2, link3, commit)
fabOnReact Jan 17, 2023
7fca70c
Run a loop over absoluteSizeSpans and check that any of them has text…
fabOnReact Jan 17, 2023
6c4e19b
removing not required prop in ViewProps.cpp
fabOnReact Jan 17, 2023
a2e3d4e
remove Nullable from ReactAbsSizeSpan constructor
fabOnReact Jan 17, 2023
5b9c505
Merge branch 'main' into nested-text-superscript
fabOnReact Jan 17, 2023
72f4726
remove changes to TextMeasureCache.h
fabOnReact Jan 17, 2023
331d945
default std::string constructor inits empty strig
fabOnReact Jan 23, 2023
e85bbe7
Merge branch 'main' into nested-text-superscript
fabOnReact Jan 31, 2023
1230e06
Move the logic for shifting text based on the lineheight to CustomSty…
fabOnReact Feb 1, 2023
c139885
Add logic to align span with smaller fontSize
fabOnReact Feb 1, 2023
5879630
remove ReactAbsoluteSizeSpan
fabOnReact Feb 1, 2023
c4d82d3
remove currentText (debug)
fabOnReact Feb 1, 2023
eb0b424
update PR based on https://github.com/facebook/react-native/pull/35949
fabOnReact Feb 9, 2023
112eeee
The parent Text does not align correctly with CustomStyleSpan with Gr…
fabOnReact Feb 9, 2023
5f76bff
removing example
fabOnReact Feb 10, 2023
a770ea8
Merge branch 'main' into nested-text-superscript
fabOnReact Feb 10, 2023
95324ec
Merge branch 'main' into nested-text-superscript
fabOnReact Feb 20, 2023
e59529d
update PR based on https://github.com/facebook/react-native/pull/35949
fabOnReact Feb 20, 2023
224c58a
update example
fabOnReact Feb 21, 2023
c9278d9
Change internal span attributes
fabOnReact Feb 22, 2023
94d644e
update comments
fabOnReact Feb 22, 2023
0e20043
update comments
fabOnReact Feb 22, 2023
d281638
update comments
fabOnReact Feb 23, 2023
46e3815
avoid change alignment with updateMeasureState
fabOnReact Feb 23, 2023
c91e24d
remove not relevant code
fabOnReact Feb 24, 2023
89b95dc
remove not relevant code
fabOnReact Feb 24, 2023
0bf75e7
update example
fabOnReact Feb 24, 2023
dbeecaa
Merge branch 'main' into nested-text-superscript
fabOnReact Feb 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
package com.facebook.react.views.text;

import android.content.res.AssetManager;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
Expand All @@ -35,6 +34,10 @@ public class CustomStyleSpan extends MetricAffectingSpan implements ReactSpan {
private final int mWeight;
private final @Nullable String mFeatureSettings;
private final @Nullable String mFontFamily;
private int mSize = 0;
private TextAlignVertical mTextAlignVertical = TextAlignVertical.CENTER;
private int mHighestLineHeight = 0;
private int mHighestFontSize = 0;

public CustomStyleSpan(
int fontStyle,
Expand All @@ -49,14 +52,61 @@ public CustomStyleSpan(
mAssetManager = assetManager;
}

public CustomStyleSpan(
int fontStyle,
int fontWeight,
@Nullable String fontFeatureSettings,
@Nullable String fontFamily,
AssetManager assetManager,
TextAlignVertical textAlignVertical,
int textSize) {
this(fontStyle, fontWeight, fontFeatureSettings, fontFamily, assetManager);
mTextAlignVertical = textAlignVertical;
mSize = textSize;
}

public enum TextAlignVertical {
TOP,
BOTTOM,
CENTER,
}

public TextAlignVertical getTextAlignVertical() {
return mTextAlignVertical;
}

public int getSize() {
return mSize;
}

@Override
public void updateDrawState(TextPaint ds) {
apply(ds, mStyle, mWeight, mFeatureSettings, mFontFamily, mAssetManager);
apply(
ds,
mStyle,
mWeight,
mFeatureSettings,
mFontFamily,
mAssetManager,
mTextAlignVertical,
mSize,
mHighestLineHeight,
mHighestFontSize);
}

@Override
public void updateMeasureState(TextPaint paint) {
apply(paint, mStyle, mWeight, mFeatureSettings, mFontFamily, mAssetManager);
apply(
paint,
mStyle,
mWeight,
mFeatureSettings,
mFontFamily,
mAssetManager,
mTextAlignVertical,
mSize,
0,
0);
}

public int getStyle() {
Expand All @@ -72,16 +122,68 @@ public int getWeight() {
}

private static void apply(
Paint paint,
TextPaint ds,
int style,
int weight,
@Nullable String fontFeatureSettings,
@Nullable String family,
AssetManager assetManager) {
AssetManager assetManager,
TextAlignVertical textAlignVertical,
int textSize,
int highestLineHeight,
int highestFontSize) {
Typeface typeface =
ReactTypefaceUtils.applyStyles(paint.getTypeface(), style, weight, family, assetManager);
paint.setFontFeatureSettings(fontFeatureSettings);
paint.setTypeface(typeface);
paint.setSubpixelText(true);
ReactTypefaceUtils.applyStyles(ds.getTypeface(), style, weight, family, assetManager);
ds.setFontFeatureSettings(fontFeatureSettings);
ds.setTypeface(typeface);
ds.setSubpixelText(true);

if (textAlignVertical == TextAlignVertical.CENTER || highestLineHeight == 0) {
return;
}

// https://stackoverflow.com/a/27631737/7295772
// top ------------- -10
// ascent ------------- -5
// baseline __my Text____ 0
// descent _____________ 2
// bottom _____________ 5
TextPaint textPaintCopy = new TextPaint();
textPaintCopy.set(ds);
if (textSize > 0) {
textPaintCopy.setTextSize(textSize);
}

if (textSize == highestFontSize) {
// aligns text vertically in the lineHeight
// and adjust their position depending on the fontSize
if (textAlignVertical == TextAlignVertical.TOP) {
ds.baselineShift -= highestLineHeight / 2 - textPaintCopy.getTextSize() / 2;
}
if (textAlignVertical == TextAlignVertical.BOTTOM) {
ds.baselineShift +=
highestLineHeight / 2 - textPaintCopy.getTextSize() / 2 - textPaintCopy.descent();
}
} else if (highestFontSize != 0 && textSize < highestFontSize) {
// aligns correctly text that has smaller font
if (textAlignVertical == TextAlignVertical.TOP) {
ds.baselineShift -=
highestLineHeight / 2
- highestFontSize / 2
// smaller font aligns on the baseline of bigger font
// moves the baseline of text with smaller font up
// so it aligns on the top of the larger font
+ (highestFontSize - textSize)
+ (textPaintCopy.getFontMetrics().top - textPaintCopy.ascent());
}
if (textAlignVertical == TextAlignVertical.BOTTOM) {
ds.baselineShift += highestLineHeight / 2 - highestFontSize / 2 - textPaintCopy.descent();
}
}
}

public void updateSpan(int highestLineHeight, int highestFontSize) {
mHighestLineHeight = highestLineHeight;
mHighestFontSize = highestFontSize;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,38 @@ public void updateExtraData(ReactTextView view, Object extraData) {
ReactAccessibilityDelegate.resetDelegate(
view, view.isFocusable(), view.getImportantForAccessibility());
}

CustomLineHeightSpan[] customLineHeightSpans =
spannable.getSpans(0, spannable.length(), CustomLineHeightSpan.class);
CustomStyleSpan[] customStyleSpans =
spannable.getSpans(0, spannable.length(), CustomStyleSpan.class);
if (customLineHeightSpans.length > 0 && customStyleSpans.length > 0) {
int highestLineHeight = 0;
for (CustomLineHeightSpan span : customLineHeightSpans) {
if (highestLineHeight == 0 || span.getLineHeight() > highestLineHeight) {
highestLineHeight = span.getLineHeight();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Retrieves the line height from the CustomLineHeightSpans (ParagraphSpan)
  2. Changes the textPaint baseline based on fontSize and lineHeight of the Paragraph and aligns the text in CustomStyleSpan (MetricAffectingSpan)

}
}

int highestFontSize = 0;
if (highestLineHeight != 0) {
for (CustomStyleSpan span : customStyleSpans) {
if (highestFontSize == 0 || span.getSize() > highestFontSize) {
highestFontSize = span.getSize();
}
}
for (CustomStyleSpan span : customStyleSpans) {
/*
* https://developer.android.com/develop/ui/views/text-and-emoji/spans#change-internal-attributes
* Changes the internal span attribute of a mutable span, such as the bullet color in a custom bullet span,
* you can avoid the overhead from calling setText() multiple times by keeping a reference to the span as it's created.
* When you need to modify the span, you can modify the reference and then call either invalidate() or requestLayout() on the TextView,
* depending on the type of attribute that you changed.
*/
span.updateSpan(highestLineHeight, highestFontSize);
Copy link
Contributor Author

@fabOnReact fabOnReact Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://developer.android.com/develop/ui/views/text-and-emoji/spans#best-practices

Calling view.setText(update) before updating the span. Result: text correctly aligns to the top or the bottom, the parent text vertical align does not change.
https://www.icloud.com/iclouddrive/0e3YRsM1KKEs0SXo0nvkEy9OQ#call_set_text_before_mutation

Calling view.setText(update) after updating the span. Result: text does not align correctly, The regression is caused by the change in the parent text vertical align.
https://www.icloud.com/iclouddrive/087kw0jNMTAFnT5pJ-mPHXzaQ#call_set_text_after_mutation

The logic is required to align spans correctly. The functionality works without issues.

}
}
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class TextAttributeProps {
public static final short TA_KEY_IS_HIGHLIGHTED = 20;
public static final short TA_KEY_LAYOUT_DIRECTION = 21;
public static final short TA_KEY_ACCESSIBILITY_ROLE = 22;
public static final short TA_KEY_ALIGN_VERTICAL = 24;

public static final int UNSET = -1;

Expand Down Expand Up @@ -100,6 +101,7 @@ public class TextAttributeProps {
protected boolean mIsUnderlineTextDecorationSet = false;
protected boolean mIsLineThroughTextDecorationSet = false;
protected boolean mIncludeFontPadding = true;
protected String mTextAlignVertical = "center-child";

protected @Nullable ReactAccessibilityDelegate.AccessibilityRole mAccessibilityRole = null;
protected boolean mIsAccessibilityRoleSet = false;
Expand Down Expand Up @@ -206,6 +208,9 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) {
case TA_KEY_ACCESSIBILITY_ROLE:
result.setAccessibilityRole(entry.getStringValue());
break;
case TA_KEY_ALIGN_VERTICAL:
result.setTextAlignVertical(entry.getStringValue());
break;
}
}

Expand Down Expand Up @@ -612,6 +617,16 @@ private void setAccessibilityRole(@Nullable String accessibilityRole) {
}
}

private void setTextAlignVertical(String alignVertical) {
if (alignVertical.equals("top")) {
mTextAlignVertical = "top-child";
} else if (alignVertical.equals("center")) {
mTextAlignVertical = "center-child";
} else if (alignVertical.equals("bottom")) {
mTextAlignVertical = "bottom-child";
}
}

public static int getTextBreakStrategy(@Nullable String textBreakStrategy) {
int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY;
if (textBreakStrategy != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,16 @@ private static void buildSpannableFromFragment(
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
if (textAttributes.mFontStyle != UNSET
|| textAttributes.mFontWeight != UNSET
|| textAttributes.mFontFamily != null) {
|| textAttributes.mFontFamily != null
|| textAttributes.mTextAlignVertical != "center-child") {
CustomStyleSpan.TextAlignVertical textAlignVertical =
CustomStyleSpan.TextAlignVertical.CENTER;
if (textAttributes.mTextAlignVertical == "top-child") {
textAlignVertical = CustomStyleSpan.TextAlignVertical.TOP;
}
if (textAttributes.mTextAlignVertical == "bottom-child") {
textAlignVertical = CustomStyleSpan.TextAlignVertical.BOTTOM;
}
ops.add(
new SetSpanOperation(
start,
Expand All @@ -172,7 +181,9 @@ private static void buildSpannableFromFragment(
textAttributes.mFontWeight,
textAttributes.mFontFeatureSettings,
textAttributes.mFontFamily,
context.getAssets())));
context.getAssets(),
textAlignVertical,
textAttributes.mFontSize)));
}
if (textAttributes.mIsUnderlineTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ void TextAttributes::apply(TextAttributes textAttributes) {
accessibilityRole = textAttributes.accessibilityRole.has_value()
? textAttributes.accessibilityRole
: accessibilityRole;
textAlignVertical = !textAttributes.textAlignVertical.empty() ? textAttributes.textAlignVertical : textAlignVertical;
}

#pragma mark - Operators
Expand All @@ -126,6 +127,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
isHighlighted,
layoutDirection,
accessibilityRole,
textAlignVertical,
textTransform) ==
std::tie(
rhs.foregroundColor,
Expand All @@ -147,6 +149,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
rhs.isHighlighted,
rhs.layoutDirection,
rhs.accessibilityRole,
rhs.textAlignVertical,
rhs.textTransform) &&
floatEquality(opacity, rhs.opacity) &&
floatEquality(fontSize, rhs.fontSize) &&
Expand Down Expand Up @@ -215,6 +218,7 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const {
debugStringConvertibleItem("isHighlighted", isHighlighted),
debugStringConvertibleItem("layoutDirection", layoutDirection),
debugStringConvertibleItem("accessibilityRole", accessibilityRole),
debugStringConvertibleItem("textAlignVertical", textAlignVertical),
};
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class TextAttributes : public DebugStringConvertible {
// construction.
std::optional<LayoutDirection> layoutDirection{};
std::optional<AccessibilityRole> accessibilityRole{};
std::string textAlignVertical{};

#pragma mark - Operations

Expand Down
9 changes: 9 additions & 0 deletions ReactCommon/react/renderer/attributedstring/conversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,10 @@ inline folly::dynamic toDynamic(const TextAttributes &textAttributes) {
_textAttributes(
"accessibilityRole", toString(*textAttributes.accessibilityRole));
}
if (!textAttributes.textAlignVertical.empty()) {
_textAttributes(
"textAlignVertical", textAttributes.textAlignVertical);
}
return _textAttributes;
}

Expand Down Expand Up @@ -1110,6 +1114,7 @@ constexpr static MapBuffer::Key TA_KEY_IS_HIGHLIGHTED = 20;
constexpr static MapBuffer::Key TA_KEY_LAYOUT_DIRECTION = 21;
constexpr static MapBuffer::Key TA_KEY_ACCESSIBILITY_ROLE = 22;
constexpr static MapBuffer::Key TA_KEY_LINE_BREAK_STRATEGY = 23;
constexpr static MapBuffer::Key TA_KEY_VERTICAL_ALIGN = 24;

// constants for ParagraphAttributes serialization
constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0;
Expand Down Expand Up @@ -1256,6 +1261,10 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) {
builder.putString(
TA_KEY_ACCESSIBILITY_ROLE, toString(*textAttributes.accessibilityRole));
}
if (!textAttributes.textAlignVertical.empty()) {
builder.putString(
TA_KEY_VERTICAL_ALIGN, textAttributes.textAlignVertical);
}
return builder.build();
}

Expand Down
9 changes: 9 additions & 0 deletions ReactCommon/react/renderer/components/text/BaseTextProps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ static TextAttributes convertRawProp(
sourceTextAttributes.backgroundColor,
defaultTextAttributes.backgroundColor);

textAttributes.textAlignVertical = convertRawProp(
context,
rawProps,
"textAlignVertical",
sourceTextAttributes.textAlignVertical,
defaultTextAttributes.textAlignVertical);

return textAttributes;
}

Expand Down Expand Up @@ -297,6 +304,8 @@ void BaseTextProps::setProp(
defaults, value, textAttributes, opacity, "opacity");
REBUILD_FIELD_SWITCH_CASE(
defaults, value, textAttributes, backgroundColor, "backgroundColor");
REBUILD_FIELD_SWITCH_CASE(
defaults, value, textAttributes, textAlignVertical, "textAlignVertical");
}
}

Expand Down
Loading