Skip to content

Commit f998a9a

Browse files
committed
improve text line height calculation on ios
1 parent 40a4feb commit f998a9a

22 files changed

Lines changed: 214 additions & 138 deletions

packages/react-native/Libraries/Text/Text/RCTTextView.mm

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#import <React/RCTUtils.h>
1313
#import <React/UIView+React.h>
14+
#import <react/featureflags/ReactNativeFeatureFlags.h>
1415

1516
#import <React/RCTTextShadowView.h>
1617

@@ -98,6 +99,36 @@ - (void)setTextStorage:(NSTextStorage *)textStorage
9899
[self setNeedsDisplay];
99100
}
100101

102+
- (CGPoint)calculateDrawingPointWithTextStorage:(NSTextStorage *)textStorage
103+
contentFrame:(CGRect)contentFrame {
104+
UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
105+
if (!font) {
106+
font = [UIFont systemFontOfSize:14];
107+
}
108+
109+
NSParagraphStyle *paragraphStyle = [textStorage attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];
110+
111+
CGFloat lineHeight = font.lineHeight;
112+
if (paragraphStyle && paragraphStyle.minimumLineHeight > 0) {
113+
lineHeight = paragraphStyle.minimumLineHeight;
114+
}
115+
116+
CGFloat ascent = font.ascender;
117+
CGFloat descent = fabs(font.descender);
118+
CGFloat textHeight = ascent + descent;
119+
120+
CGFloat verticalOffset = 0;
121+
if (textHeight > lineHeight) {
122+
CGFloat difference = textHeight - lineHeight;
123+
verticalOffset = difference / 2.0;
124+
} else if (textHeight < lineHeight) {
125+
CGFloat difference = lineHeight - textHeight;
126+
verticalOffset = -(difference / 2.0);
127+
}
128+
129+
return CGPointMake(contentFrame.origin.x, contentFrame.origin.y + verticalOffset);
130+
}
131+
101132
- (void)drawRect:(CGRect)rect
102133
{
103134
[super drawRect:rect];
@@ -118,8 +149,15 @@ - (void)drawRect:(CGRect)rect
118149
#endif
119150

120151
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
121-
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:_contentFrame.origin];
122-
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_contentFrame.origin];
152+
153+
if (ReactNativeFeatureFlags::enableLineHeightCentering()) {
154+
CGPoint drawingPoint = [self calculateDrawingPointWithTextStorage:_textStorage contentFrame:_contentFrame];
155+
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:drawingPoint];
156+
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:drawingPoint];
157+
} else {
158+
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:_contentFrame.origin];
159+
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_contentFrame.origin];
160+
}
123161

124162
__block UIBezierPath *highlightPath = nil;
125163
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];

packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import "RCTParagraphComponentAccessibilityProvider.h"
1010

1111
#import <MobileCoreServices/UTCoreTypes.h>
12+
#import <react/featureflags/ReactNativeFeatureFlags.h>
1213
#import <react/renderer/components/text/ParagraphComponentDescriptor.h>
1314
#import <react/renderer/components/text/ParagraphProps.h>
1415
#import <react/renderer/components/text/ParagraphState.h>
@@ -326,6 +327,38 @@ @implementation RCTParagraphTextView {
326327
CAShapeLayer *_highlightLayer;
327328
}
328329

330+
- (CGRect)calculateCenteredFrameWithAttributedText:(NSAttributedString *)attributedText
331+
frame:(CGRect)frame {
332+
UIFont *font = [attributedText attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
333+
if (!font) {
334+
font = [UIFont systemFontOfSize:14];
335+
}
336+
337+
NSParagraphStyle *paragraphStyle = [attributedText attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];
338+
CGFloat lineHeight = font.lineHeight;
339+
340+
if (paragraphStyle && paragraphStyle.minimumLineHeight > 0) {
341+
lineHeight = paragraphStyle.minimumLineHeight;
342+
}
343+
344+
CGFloat ascent = font.ascender;
345+
CGFloat descent = fabs(font.descender);
346+
CGFloat textHeight = ascent + descent;
347+
348+
CGFloat verticalOffset = 0;
349+
if (textHeight > lineHeight) {
350+
CGFloat difference = textHeight - lineHeight;
351+
verticalOffset = difference / 2.0;
352+
} else if (textHeight < lineHeight) {
353+
CGFloat difference = lineHeight - textHeight;
354+
verticalOffset = -(difference / 2.0);
355+
}
356+
357+
frame.origin.y += verticalOffset;
358+
359+
return frame;
360+
}
361+
329362
- (void)drawRect:(CGRect)rect
330363
{
331364
if (!_state) {
@@ -343,6 +376,11 @@ - (void)drawRect:(CGRect)rect
343376

344377
CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
345378

379+
if (ReactNativeFeatureFlags::enableLineHeightCentering()) {
380+
NSAttributedString *attributedText = RCTNSAttributedStringFromAttributedString(_state->getData().attributedString);
381+
frame = [self calculateCenteredFrameWithAttributedText:attributedText frame:frame];
382+
}
383+
346384
[nativeTextLayoutManager drawAttributedString:_state->getData().attributedString
347385
paragraphAttributes:_paragraphAttributes
348386
frame:frame

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<7d80322a6a37083c5e52e6914de49ce2>>
7+
* @generated SignedSource<<ac76caa581b2c41bc95ed973bbd27b99>>
88
*/
99

1010
/**
@@ -58,12 +58,6 @@ public object ReactNativeFeatureFlags {
5858
@JvmStatic
5959
public fun enableAlignItemsBaselineOnFabricIOS(): Boolean = accessor.enableAlignItemsBaselineOnFabricIOS()
6060

61-
/**
62-
* When enabled, custom line height calculation will be centered from top to bottom.
63-
*/
64-
@JvmStatic
65-
public fun enableAndroidLineHeightCentering(): Boolean = accessor.enableAndroidLineHeightCentering()
66-
6761
/**
6862
* Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable the following flags: `useTurboModules` & `enableFabricRenderer.
6963
*/
@@ -130,6 +124,12 @@ public object ReactNativeFeatureFlags {
130124
@JvmStatic
131125
public fun enableLayoutAnimationsOnIOS(): Boolean = accessor.enableLayoutAnimationsOnIOS()
132126

127+
/**
128+
* When enabled, custom line height calculation will be centered from top to bottom.
129+
*/
130+
@JvmStatic
131+
public fun enableLineHeightCentering(): Boolean = accessor.enableLineHeightCentering()
132+
133133
/**
134134
* Enables the reporting of long tasks through `PerformanceObserver`. Only works if the event loop is enabled.
135135
*/

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<761d3e7b100a4f5ee6f8bda71f84918b>>
7+
* @generated SignedSource<<ee55d752b258637159b99ba75e217395>>
88
*/
99

1010
/**
@@ -25,7 +25,6 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
2525
private var batchRenderingUpdatesInEventLoopCache: Boolean? = null
2626
private var completeReactInstanceCreationOnBgThreadOnAndroidCache: Boolean? = null
2727
private var enableAlignItemsBaselineOnFabricIOSCache: Boolean? = null
28-
private var enableAndroidLineHeightCenteringCache: Boolean? = null
2928
private var enableBridgelessArchitectureCache: Boolean? = null
3029
private var enableCleanTextInputYogaNodeCache: Boolean? = null
3130
private var enableDeletionOfUnmountedViewsCache: Boolean? = null
@@ -37,6 +36,7 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
3736
private var enableGranularShadowTreeStateReconciliationCache: Boolean? = null
3837
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
3938
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
39+
private var enableLineHeightCenteringCache: Boolean? = null
4040
private var enableLongTaskAPICache: Boolean? = null
4141
private var enableMicrotasksCache: Boolean? = null
4242
private var enablePreciseSchedulingForPremountItemsOnAndroidCache: Boolean? = null
@@ -114,15 +114,6 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
114114
return cached
115115
}
116116

117-
override fun enableAndroidLineHeightCentering(): Boolean {
118-
var cached = enableAndroidLineHeightCenteringCache
119-
if (cached == null) {
120-
cached = ReactNativeFeatureFlagsCxxInterop.enableAndroidLineHeightCentering()
121-
enableAndroidLineHeightCenteringCache = cached
122-
}
123-
return cached
124-
}
125-
126117
override fun enableBridgelessArchitecture(): Boolean {
127118
var cached = enableBridgelessArchitectureCache
128119
if (cached == null) {
@@ -222,6 +213,15 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
222213
return cached
223214
}
224215

216+
override fun enableLineHeightCentering(): Boolean {
217+
var cached = enableLineHeightCenteringCache
218+
if (cached == null) {
219+
cached = ReactNativeFeatureFlagsCxxInterop.enableLineHeightCentering()
220+
enableLineHeightCenteringCache = cached
221+
}
222+
return cached
223+
}
224+
225225
override fun enableLongTaskAPI(): Boolean {
226226
var cached = enableLongTaskAPICache
227227
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1ed46e0bf712406c1bf6159e1bca3c15>>
7+
* @generated SignedSource<<eb10b9718add54a31ee41200c8b48076>>
88
*/
99

1010
/**
@@ -38,8 +38,6 @@ public object ReactNativeFeatureFlagsCxxInterop {
3838

3939
@DoNotStrip @JvmStatic public external fun enableAlignItemsBaselineOnFabricIOS(): Boolean
4040

41-
@DoNotStrip @JvmStatic public external fun enableAndroidLineHeightCentering(): Boolean
42-
4341
@DoNotStrip @JvmStatic public external fun enableBridgelessArchitecture(): Boolean
4442

4543
@DoNotStrip @JvmStatic public external fun enableCleanTextInputYogaNode(): Boolean
@@ -62,6 +60,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
6260

6361
@DoNotStrip @JvmStatic public external fun enableLayoutAnimationsOnIOS(): Boolean
6462

63+
@DoNotStrip @JvmStatic public external fun enableLineHeightCentering(): Boolean
64+
6565
@DoNotStrip @JvmStatic public external fun enableLongTaskAPI(): Boolean
6666

6767
@DoNotStrip @JvmStatic public external fun enableMicrotasks(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<19cf402242ebd8b3a08dfb7c755b801b>>
7+
* @generated SignedSource<<3c8abaac37fc7644f7ace96919390074>>
88
*/
99

1010
/**
@@ -33,8 +33,6 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
3333

3434
override fun enableAlignItemsBaselineOnFabricIOS(): Boolean = true
3535

36-
override fun enableAndroidLineHeightCentering(): Boolean = false
37-
3836
override fun enableBridgelessArchitecture(): Boolean = false
3937

4038
override fun enableCleanTextInputYogaNode(): Boolean = false
@@ -57,6 +55,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
5755

5856
override fun enableLayoutAnimationsOnIOS(): Boolean = true
5957

58+
override fun enableLineHeightCentering(): Boolean = false
59+
6060
override fun enableLongTaskAPI(): Boolean = false
6161

6262
override fun enableMicrotasks(): Boolean = false

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<a307a1f9b5064547ce32f9519bbf27c4>>
7+
* @generated SignedSource<<f726bd566cebccbd10dac4ec6d2b974d>>
88
*/
99

1010
/**
@@ -29,7 +29,6 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
2929
private var batchRenderingUpdatesInEventLoopCache: Boolean? = null
3030
private var completeReactInstanceCreationOnBgThreadOnAndroidCache: Boolean? = null
3131
private var enableAlignItemsBaselineOnFabricIOSCache: Boolean? = null
32-
private var enableAndroidLineHeightCenteringCache: Boolean? = null
3332
private var enableBridgelessArchitectureCache: Boolean? = null
3433
private var enableCleanTextInputYogaNodeCache: Boolean? = null
3534
private var enableDeletionOfUnmountedViewsCache: Boolean? = null
@@ -41,6 +40,7 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
4140
private var enableGranularShadowTreeStateReconciliationCache: Boolean? = null
4241
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
4342
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
43+
private var enableLineHeightCenteringCache: Boolean? = null
4444
private var enableLongTaskAPICache: Boolean? = null
4545
private var enableMicrotasksCache: Boolean? = null
4646
private var enablePreciseSchedulingForPremountItemsOnAndroidCache: Boolean? = null
@@ -123,16 +123,6 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
123123
return cached
124124
}
125125

126-
override fun enableAndroidLineHeightCentering(): Boolean {
127-
var cached = enableAndroidLineHeightCenteringCache
128-
if (cached == null) {
129-
cached = currentProvider.enableAndroidLineHeightCentering()
130-
accessedFeatureFlags.add("enableAndroidLineHeightCentering")
131-
enableAndroidLineHeightCenteringCache = cached
132-
}
133-
return cached
134-
}
135-
136126
override fun enableBridgelessArchitecture(): Boolean {
137127
var cached = enableBridgelessArchitectureCache
138128
if (cached == null) {
@@ -243,6 +233,16 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
243233
return cached
244234
}
245235

236+
override fun enableLineHeightCentering(): Boolean {
237+
var cached = enableLineHeightCenteringCache
238+
if (cached == null) {
239+
cached = currentProvider.enableLineHeightCentering()
240+
accessedFeatureFlags.add("enableLineHeightCentering")
241+
enableLineHeightCenteringCache = cached
242+
}
243+
return cached
244+
}
245+
246246
override fun enableLongTaskAPI(): Boolean {
247247
var cached = enableLongTaskAPICache
248248
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<bda9e94a5fe61a16e7d001ea3acfed0c>>
7+
* @generated SignedSource<<73688196634e93c5b6ea63181e13f738>>
88
*/
99

1010
/**
@@ -33,8 +33,6 @@ public interface ReactNativeFeatureFlagsProvider {
3333

3434
@DoNotStrip public fun enableAlignItemsBaselineOnFabricIOS(): Boolean
3535

36-
@DoNotStrip public fun enableAndroidLineHeightCentering(): Boolean
37-
3836
@DoNotStrip public fun enableBridgelessArchitecture(): Boolean
3937

4038
@DoNotStrip public fun enableCleanTextInputYogaNode(): Boolean
@@ -57,6 +55,8 @@ public interface ReactNativeFeatureFlagsProvider {
5755

5856
@DoNotStrip public fun enableLayoutAnimationsOnIOS(): Boolean
5957

58+
@DoNotStrip public fun enableLineHeightCentering(): Boolean
59+
6060
@DoNotStrip public fun enableLongTaskAPI(): Boolean
6161

6262
@DoNotStrip public fun enableMicrotasks(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpan.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public class CustomLineHeightSpan(height: Float) : LineHeightSpan, ReactSpan {
107107
v: Int,
108108
fm: FontMetricsInt,
109109
) {
110-
if (ReactNativeFeatureFlags.enableAndroidLineHeightCentering()) chooseCenteredHeight(fm)
110+
if (ReactNativeFeatureFlags.enableLineHeightCentering()) chooseCenteredHeight(fm)
111111
else chooseOriginalHeight(fm)
112112
}
113113
}

0 commit comments

Comments
 (0)