Skip to content

Commit 027ff55

Browse files
authored
Support React Native 0.77 (#600)
1 parent 2069cec commit 027ff55

File tree

25 files changed

+1431
-1356
lines changed

25 files changed

+1431
-1356
lines changed

android/build.gradle

+11-2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ def getReactNativeMinorVersion() {
6969
return reactNativeVersion.split("\\.")[1].toInteger()
7070
}
7171

72+
def REACT_NATIVE_MINOR_VERSION = getReactNativeMinorVersion()
73+
7274
android {
7375
if (supportsNamespace()) {
7476
namespace "com.expensify.livemarkdown"
@@ -86,7 +88,7 @@ android {
8688
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
8789
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
8890

89-
buildConfigField "int", "REACT_NATIVE_MINOR_VERSION", getReactNativeMinorVersion().toString()
91+
buildConfigField "int", "REACT_NATIVE_MINOR_VERSION", REACT_NATIVE_MINOR_VERSION.toString()
9092
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
9193

9294
consumerProguardFiles "proguard-rules.pro"
@@ -95,7 +97,7 @@ android {
9597
cmake {
9698
arguments "-DANDROID_STL=c++_shared",
9799
"-DANDROID_TOOLCHAIN=clang",
98-
"-DREACT_NATIVE_MINOR_VERSION=${getReactNativeMinorVersion()}"
100+
"-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}"
99101
abiFilters (*reactNativeArchitectures())
100102
}
101103
}
@@ -132,6 +134,13 @@ android {
132134
} else {
133135
java.srcDirs += ["src/oldarch"]
134136
}
137+
138+
// TextLayoutManager
139+
if (REACT_NATIVE_MINOR_VERSION <= 76 ) {
140+
java.srcDirs += "src/reactNativeVersionPatch/CustomMountingManager/76"
141+
} else {
142+
java.srcDirs += "src/reactNativeVersionPatch/CustomMountingManager/latest"
143+
}
135144
}
136145
}
137146

android/gradle.properties

-5
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package com.expensify.livemarkdown;
2+
3+
import static com.facebook.react.views.text.TextAttributeProps.UNSET;
4+
5+
import android.content.Context;
6+
import android.content.res.AssetManager;
7+
import android.text.BoringLayout;
8+
import android.text.Layout;
9+
import android.text.Spannable;
10+
import android.text.SpannableStringBuilder;
11+
import android.text.TextPaint;
12+
13+
import androidx.annotation.NonNull;
14+
import androidx.annotation.Nullable;
15+
import androidx.core.util.Preconditions;
16+
17+
import com.facebook.react.bridge.ReactContext;
18+
import com.facebook.react.bridge.ReadableMap;
19+
import com.facebook.react.common.mapbuffer.MapBuffer;
20+
import com.facebook.react.fabric.mounting.MountingManager;
21+
import com.facebook.react.uimanager.PixelUtil;
22+
import com.facebook.react.uimanager.ViewManagerRegistry;
23+
import com.facebook.react.views.text.TextAttributeProps;
24+
import com.facebook.react.views.text.TextLayoutManager;
25+
import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan;
26+
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan;
27+
import com.facebook.yoga.YogaMeasureMode;
28+
import com.facebook.yoga.YogaMeasureOutput;
29+
30+
import java.lang.reflect.InvocationTargetException;
31+
import java.lang.reflect.Method;
32+
33+
public class CustomMountingManager extends MountingManager {
34+
private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true;
35+
private static final ThreadLocal<TextPaint> sTextPaintInstance =
36+
new ThreadLocal<TextPaint>() {
37+
@Override
38+
protected TextPaint initialValue() {
39+
return new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
40+
}
41+
};
42+
43+
private MarkdownUtils markdownUtils;
44+
45+
public CustomMountingManager(
46+
@NonNull ViewManagerRegistry viewManagerRegistry,
47+
@NonNull MountItemExecutor mountItemExecutor,
48+
@NonNull Context context,
49+
@NonNull ReadableMap decoratorProps,
50+
int parserId) {
51+
super(viewManagerRegistry, mountItemExecutor);
52+
this.markdownUtils = new MarkdownUtils((ReactContext) context);
53+
this.markdownUtils.setMarkdownStyle(new MarkdownStyle(decoratorProps, context));
54+
this.markdownUtils.setParserId(parserId);
55+
}
56+
57+
@Override
58+
public long measureMapBuffer(
59+
@NonNull ReactContext context,
60+
@NonNull String componentName,
61+
@NonNull MapBuffer attributedString,
62+
@NonNull MapBuffer paragraphAttributes,
63+
@Nullable MapBuffer state,
64+
float width,
65+
@NonNull YogaMeasureMode widthYogaMeasureMode,
66+
float height,
67+
@NonNull YogaMeasureMode heightYogaMeasureMode,
68+
@Nullable float[] attachmentsPositions) {
69+
70+
Spannable text =
71+
TextLayoutManager.getOrCreateSpannableForText(context, attributedString, null);
72+
73+
if (text == null) {
74+
return 0;
75+
}
76+
77+
int textBreakStrategy =
78+
TextAttributeProps.getTextBreakStrategy(
79+
paragraphAttributes.getString(TextLayoutManager.PA_KEY_TEXT_BREAK_STRATEGY));
80+
boolean includeFontPadding =
81+
paragraphAttributes.contains(TextLayoutManager.PA_KEY_INCLUDE_FONT_PADDING)
82+
? paragraphAttributes.getBoolean(TextLayoutManager.PA_KEY_INCLUDE_FONT_PADDING)
83+
: DEFAULT_INCLUDE_FONT_PADDING;
84+
int hyphenationFrequency =
85+
TextAttributeProps.getHyphenationFrequency(
86+
paragraphAttributes.getString(TextLayoutManager.PA_KEY_HYPHENATION_FREQUENCY));
87+
88+
try {
89+
Class<TextLayoutManager> textLayoutManagerClass = TextLayoutManager.class;
90+
91+
Method getTextAlignmentAttrMethod = textLayoutManagerClass.getDeclaredMethod("getTextAlignmentAttr", MapBuffer.class);
92+
getTextAlignmentAttrMethod.setAccessible(true);
93+
94+
String textAlignmentAttr = (String)getTextAlignmentAttrMethod.invoke(null, attributedString);
95+
96+
Method getTextAlignmentMethod = textLayoutManagerClass.getDeclaredMethod("getTextAlignment", MapBuffer.class, Spannable.class, String.class);
97+
getTextAlignmentMethod.setAccessible(true);
98+
99+
Layout.Alignment alignment = (Layout.Alignment)getTextAlignmentMethod.invoke(
100+
null,
101+
attributedString,
102+
text,
103+
textAlignmentAttr
104+
);
105+
106+
Method getTextJustificationModeMethod = textLayoutManagerClass.getDeclaredMethod("getTextJustificationMode", String.class);
107+
getTextJustificationModeMethod.setAccessible(true);
108+
109+
Integer justificationMode = (Integer) getTextJustificationModeMethod.invoke(null, textAlignmentAttr);
110+
111+
112+
markdownUtils.applyMarkdownFormatting((SpannableStringBuilder)text);
113+
114+
TextPaint paint;
115+
if (attributedString.contains(TextLayoutManager.AS_KEY_CACHE_ID)) {
116+
paint = text.getSpans(0, 0, ReactTextPaintHolderSpan.class)[0].getTextPaint();
117+
} else {
118+
TextAttributeProps baseTextAttributes =
119+
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(TextLayoutManager.AS_KEY_BASE_ATTRIBUTES));
120+
paint = Preconditions.checkNotNull(sTextPaintInstance.get());
121+
122+
Method updateTextPaintMethod = textLayoutManagerClass.getDeclaredMethod("updateTextPaint", TextPaint.class, TextAttributeProps.class, Context.class);
123+
updateTextPaintMethod.setAccessible(true);
124+
updateTextPaintMethod.invoke(null, paint, baseTextAttributes, context);
125+
}
126+
127+
BoringLayout.Metrics boring = BoringLayout.isBoring(text, paint);
128+
129+
Method createLayoutMethod = textLayoutManagerClass.getDeclaredMethod("createLayout", Spannable.class, BoringLayout.Metrics.class, float.class, YogaMeasureMode.class, boolean.class, int.class, int.class, Layout.Alignment.class, int.class, TextPaint.class);
130+
createLayoutMethod.setAccessible(true);
131+
132+
Layout layout = (Layout)createLayoutMethod.invoke(
133+
null,
134+
text,
135+
boring,
136+
width,
137+
widthYogaMeasureMode,
138+
includeFontPadding,
139+
textBreakStrategy,
140+
hyphenationFrequency,
141+
alignment,
142+
justificationMode,
143+
paint);
144+
145+
int maximumNumberOfLines =
146+
paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
147+
? paragraphAttributes.getInt(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
148+
: UNSET;
149+
150+
int calculatedLineCount =
151+
maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
152+
? layout.getLineCount()
153+
: Math.min(maximumNumberOfLines, layout.getLineCount());
154+
155+
// Instead of using `layout.getWidth()` (which may yield a significantly larger width for
156+
// text that is wrapping), compute width using the longest line.
157+
float calculatedWidth = 0;
158+
if (widthYogaMeasureMode == YogaMeasureMode.EXACTLY) {
159+
calculatedWidth = width;
160+
} else {
161+
for (int lineIndex = 0; lineIndex < calculatedLineCount; lineIndex++) {
162+
boolean endsWithNewLine =
163+
text.length() > 0 && text.charAt(layout.getLineEnd(lineIndex) - 1) == '\n';
164+
float lineWidth =
165+
endsWithNewLine ? layout.getLineMax(lineIndex) : layout.getLineWidth(lineIndex);
166+
if (lineWidth > calculatedWidth) {
167+
calculatedWidth = lineWidth;
168+
}
169+
}
170+
if (widthYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedWidth > width) {
171+
calculatedWidth = width;
172+
}
173+
}
174+
175+
// Android 11+ introduces changes in text width calculation which leads to cases
176+
// where the container is measured smaller than text. Math.ceil prevents it
177+
// See T136756103 for investigation
178+
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.Q) {
179+
calculatedWidth = (float) Math.ceil(calculatedWidth);
180+
}
181+
182+
float calculatedHeight = height;
183+
if (heightYogaMeasureMode != YogaMeasureMode.EXACTLY) {
184+
calculatedHeight = layout.getLineBottom(calculatedLineCount - 1);
185+
if (heightYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedHeight > height) {
186+
calculatedHeight = height;
187+
}
188+
}
189+
190+
// Calculate the positions of the attachments (views) that will be rendered inside the
191+
// Spanned Text. The following logic is only executed when a text contains views inside.
192+
// This follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method).
193+
int attachmentIndex = 0;
194+
int lastAttachmentFoundInSpan;
195+
for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) {
196+
lastAttachmentFoundInSpan =
197+
text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class);
198+
TextInlineViewPlaceholderSpan[] placeholders =
199+
text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class);
200+
for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
201+
int start = text.getSpanStart(placeholder);
202+
int line = layout.getLineForOffset(start);
203+
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
204+
// This truncation check works well on recent versions of Android (tested on 5.1.1 and
205+
// 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on
206+
// Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the
207+
// first thing to be truncated.
208+
if (!(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line))
209+
|| start >= layout.getLineEnd(line)) {
210+
float placeholderWidth = placeholder.getWidth();
211+
float placeholderHeight = placeholder.getHeight();
212+
// Calculate if the direction of the placeholder character is Right-To-Left.
213+
boolean isRtlChar = layout.isRtlCharAt(start);
214+
boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
215+
float placeholderLeftPosition;
216+
// There's a bug on Samsung devices where calling getPrimaryHorizontal on
217+
// the last offset in the layout will result in an endless loop. Work around
218+
// this bug by avoiding getPrimaryHorizontal in that case.
219+
if (start == text.length() - 1) {
220+
boolean endsWithNewLine =
221+
text.length() > 0 && text.charAt(layout.getLineEnd(line) - 1) == '\n';
222+
float lineWidth = endsWithNewLine ? layout.getLineMax(line) : layout.getLineWidth(line);
223+
placeholderLeftPosition =
224+
isRtlParagraph
225+
// Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns
226+
// incorrect
227+
// values when the paragraph is RTL and `setSingleLine(true)`.
228+
? calculatedWidth - lineWidth
229+
: layout.getLineRight(line) - placeholderWidth;
230+
} else {
231+
// The direction of the paragraph may not be exactly the direction the string is
232+
// heading
233+
// in at the
234+
// position of the placeholder. So, if the direction of the character is the same
235+
// as the
236+
// paragraph
237+
// use primary, secondary otherwise.
238+
boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar;
239+
placeholderLeftPosition =
240+
characterAndParagraphDirectionMatch
241+
? layout.getPrimaryHorizontal(start)
242+
: layout.getSecondaryHorizontal(start);
243+
if (isRtlParagraph) {
244+
// Adjust `placeholderLeftPosition` to work around an Android bug.
245+
// The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout
246+
// methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and
247+
// `getLineRight` return incorrect values. Their return values seem to be off
248+
// by the same number of pixels so subtracting these values cancels out the
249+
// error.
250+
//
251+
// The result is equivalent to bugless versions of
252+
// `getPrimaryHorizontal`/`getSecondaryHorizontal`.
253+
placeholderLeftPosition =
254+
calculatedWidth - (layout.getLineRight(line) - placeholderLeftPosition);
255+
}
256+
if (isRtlChar) {
257+
placeholderLeftPosition -= placeholderWidth;
258+
}
259+
}
260+
// Vertically align the inline view to the baseline of the line of text.
261+
float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight;
262+
int attachmentPosition = attachmentIndex * 2;
263+
264+
// The attachment array returns the positions of each of the attachments as
265+
attachmentsPositions[attachmentPosition] =
266+
PixelUtil.toDIPFromPixel(placeholderTopPosition);
267+
attachmentsPositions[attachmentPosition + 1] =
268+
PixelUtil.toDIPFromPixel(placeholderLeftPosition);
269+
attachmentIndex++;
270+
}
271+
}
272+
}
273+
274+
float widthInSP = PixelUtil.toDIPFromPixel(calculatedWidth);
275+
float heightInSP = PixelUtil.toDIPFromPixel(calculatedHeight);
276+
277+
return YogaMeasureOutput.make(widthInSP, heightInSP);
278+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
279+
throw new RuntimeException(e);
280+
}
281+
}
282+
}

example/Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ ruby ">= 2.6.10"
77
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
88
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
99
gem 'xcodeproj', '< 1.26.0'
10+
gem 'concurrent-ruby', '< 1.3.4'

0 commit comments

Comments
 (0)