Text: send metrics after rendering (iOS)
Summary: This adds a callback for <Text> to get metrics about the rendered text. It's divided by line but that could be changed to "fragments" (which makes more sense for multi-lingual). Right now by line is convenient as you frequently want to know where the first and last line end (though we could make this work with fragments I suppose). Reviewed By: shergin Differential Revision: D9440914 fbshipit-source-id: bb011bb7a52438380d3f604ffe7019b98c18d978
This commit is contained in:
parent
1f96ff62cf
commit
64a52532fe
|
@ -64,6 +64,12 @@ const viewConfig = {
|
|||
adjustsFontSizeToFit: true,
|
||||
minimumFontScale: true,
|
||||
textBreakStrategy: true,
|
||||
onTextLayout: true,
|
||||
},
|
||||
directEventTypes: {
|
||||
topTextLayout: {
|
||||
registrationName: 'onTextLayout',
|
||||
},
|
||||
},
|
||||
uiViewClassName: 'RCTText',
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property (nonatomic, assign) NSLineBreakMode lineBreakMode;
|
||||
@property (nonatomic, assign) BOOL adjustsFontSizeToFit;
|
||||
@property (nonatomic, assign) CGFloat minimumFontScale;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onTextLayout;
|
||||
|
||||
- (void)uiManagerWillPerformMounting;
|
||||
|
||||
|
|
|
@ -304,6 +304,35 @@
|
|||
[shadowView layoutWithMetrics:localLayoutMetrics layoutContext:localLayoutContext];
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
if (_onTextLayout) {
|
||||
NSMutableArray *lineData = [NSMutableArray new];
|
||||
[layoutManager
|
||||
enumerateLineFragmentsForGlyphRange:glyphRange
|
||||
usingBlock:^(CGRect overallRect, CGRect usedRect, NSTextContainer * _Nonnull usedTextContainer, NSRange lineGlyphRange, BOOL * _Nonnull stop) {
|
||||
NSRange range = [layoutManager characterRangeForGlyphRange:lineGlyphRange actualGlyphRange:nil];
|
||||
NSString *renderedString = [textStorage.string substringWithRange:range];
|
||||
UIFont *font = [[textStorage attributedSubstringFromRange:range] attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
|
||||
[lineData addObject:
|
||||
@{
|
||||
@"text": renderedString,
|
||||
@"x": @(usedRect.origin.x),
|
||||
@"y": @(usedRect.origin.y),
|
||||
@"width": @(usedRect.size.width),
|
||||
@"height": @(usedRect.size.height),
|
||||
@"descender": @(-font.descender),
|
||||
@"capHeight": @(font.capHeight),
|
||||
@"ascender": @(font.ascender),
|
||||
@"xHeight": @(font.xHeight),
|
||||
}];
|
||||
}];
|
||||
NSDictionary *payload =
|
||||
@{
|
||||
@"lines": lineData,
|
||||
};
|
||||
_onTextLayout(payload);
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)lastBaselineForSize:(CGSize)size
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import <React/RCTComponent.h>
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -34,6 +34,8 @@ RCT_REMAP_SHADOW_PROPERTY(ellipsizeMode, lineBreakMode, NSLineBreakMode)
|
|||
RCT_REMAP_SHADOW_PROPERTY(adjustsFontSizeToFit, adjustsFontSizeToFit, BOOL)
|
||||
RCT_REMAP_SHADOW_PROPERTY(minimumFontScale, minimumFontScale, CGFloat)
|
||||
|
||||
RCT_EXPORT_SHADOW_PROPERTY(onTextLayout, RCTDirectEventBlock)
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
|
||||
|
||||
- (void)setBridge:(RCTBridge *)bridge
|
||||
|
|
|
@ -14,7 +14,15 @@ const Platform = require('Platform');
|
|||
var React = require('react');
|
||||
var createReactClass = require('create-react-class');
|
||||
var ReactNative = require('react-native');
|
||||
var {Image, Text, TextInput, View, LayoutAnimation, Button} = ReactNative;
|
||||
var {
|
||||
Image,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
LayoutAnimation,
|
||||
Button,
|
||||
Picker,
|
||||
} = ReactNative;
|
||||
|
||||
type TextAlignExampleRTLState = {|
|
||||
isRTL: boolean,
|
||||
|
@ -275,6 +283,311 @@ class TextBaseLineLayoutExample extends React.Component<*, *> {
|
|||
}
|
||||
}
|
||||
|
||||
class TextRenderInfoExample extends React.Component<*, *> {
|
||||
state = {
|
||||
textMetrics: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
capHeight: 0,
|
||||
descender: 0,
|
||||
ascender: 0,
|
||||
xHeight: 0,
|
||||
},
|
||||
numberOfTextBlocks: 1,
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
render() {
|
||||
const topOfBox =
|
||||
this.state.textMetrics.y +
|
||||
this.state.textMetrics.height -
|
||||
(this.state.textMetrics.descender + this.state.textMetrics.capHeight);
|
||||
return (
|
||||
<View>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: this.state.textMetrics.x + this.state.textMetrics.width,
|
||||
top: topOfBox,
|
||||
width: 5,
|
||||
height: Math.ceil(
|
||||
this.state.textMetrics.capHeight -
|
||||
this.state.textMetrics.xHeight,
|
||||
),
|
||||
backgroundColor: 'red',
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: this.state.textMetrics.x + this.state.textMetrics.width,
|
||||
top:
|
||||
topOfBox +
|
||||
(this.state.textMetrics.capHeight -
|
||||
this.state.textMetrics.xHeight),
|
||||
width: 5,
|
||||
height: Math.ceil(this.state.textMetrics.xHeight),
|
||||
backgroundColor: 'green',
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{fontSize: this.state.fontSize}}
|
||||
onTextLayout={event => {
|
||||
const {lines} = event.nativeEvent;
|
||||
if (lines.length > 0) {
|
||||
this.setState({textMetrics: lines[lines.length - 1]});
|
||||
}
|
||||
}}>
|
||||
{new Array(this.state.numberOfTextBlocks)
|
||||
.fill('A tiny block of text.')
|
||||
.join(' ')}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
onPress={() =>
|
||||
this.setState({
|
||||
numberOfTextBlocks: this.state.numberOfTextBlocks + 1,
|
||||
})
|
||||
}>
|
||||
More text
|
||||
</Text>
|
||||
<Text
|
||||
onPress={() => this.setState({fontSize: this.state.fontSize + 1})}>
|
||||
Increase size
|
||||
</Text>
|
||||
<Text
|
||||
onPress={() => this.setState({fontSize: this.state.fontSize - 1})}>
|
||||
Decrease size
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextWithCapBaseBox extends React.Component<*, *> {
|
||||
state = {
|
||||
textMetrics: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
capHeight: 0,
|
||||
descender: 0,
|
||||
ascender: 0,
|
||||
xHeight: 0,
|
||||
},
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<Text
|
||||
onTextLayout={event => {
|
||||
const {lines} = event.nativeEvent;
|
||||
if (lines.length > 0) {
|
||||
this.setState({textMetrics: lines[0]});
|
||||
}
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
marginTop: Math.ceil(
|
||||
-(
|
||||
this.state.textMetrics.ascender -
|
||||
this.state.textMetrics.capHeight
|
||||
),
|
||||
),
|
||||
marginBottom: Math.ceil(-this.state.textMetrics.descender),
|
||||
},
|
||||
this.props.style,
|
||||
]}>
|
||||
{this.props.children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextLegend extends React.Component<*, *> {
|
||||
state = {
|
||||
textMetrics: [],
|
||||
language: 'english',
|
||||
};
|
||||
|
||||
render() {
|
||||
const PANGRAMS = {
|
||||
arabic:
|
||||
'صِف خَلقَ خَودِ كَمِثلِ الشَمسِ إِذ بَزَغَت — يَحظى الضَجيعُ بِها نَجلاءَ مِعطارِ',
|
||||
chinese: 'Innovation in China 中国智造,慧及全球 0123456789',
|
||||
english: 'The quick brown fox jumps over the lazy dog.',
|
||||
emoji: '🙏🏾🚗💩😍🤯👩🏽🔧🇨🇦💯',
|
||||
german: 'Falsches Üben von Xylophonmusik quält jeden größeren Zwerg',
|
||||
greek: 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός',
|
||||
hebrew: 'דג סקרן שט בים מאוכזב ולפתע מצא חברה',
|
||||
hindi:
|
||||
'ऋषियों को सताने वाले दुष्ट राक्षसों के राजा रावण का सर्वनाश करने वाले विष्णुवतार भगवान श्रीराम, अयोध्या के महाराज दशरथ के बड़े सपुत्र थे।',
|
||||
igbo:
|
||||
'Nne, nna, wepụ he’l’ụjọ dum n’ime ọzụzụ ụmụ, vufesi obi nye Chukwu, ṅụrịanụ, gbakọọnụ kpaa, kwee ya ka o guzoshie ike; ọ ghaghị ito, nwapụta ezi agwa',
|
||||
irish:
|
||||
'D’fhuascail Íosa Úrmhac na hÓighe Beannaithe pór Éava agus Ádhaimh',
|
||||
japanese:
|
||||
'色は匂へど 散りぬるを 我が世誰ぞ 常ならむ 有為の奥山 今日越えて 浅き夢見じ 酔ひもせず',
|
||||
korean:
|
||||
'키스의 고유조건은 입술끼리 만나야 하고 특별한 기술은 필요치 않다',
|
||||
norwegian:
|
||||
'Vår sære Zulu fra badeøya spilte jo whist og quickstep i min taxi.',
|
||||
polish: 'Jeżu klątw, spłódź Finom część gry hańb!',
|
||||
romanian: 'Muzicologă în bej vând whisky și tequila, preț fix.',
|
||||
russian: 'Эх, чужак, общий съём цен шляп (юфть) – вдрызг!',
|
||||
swedish: 'Yxskaftbud, ge vår WC-zonmö IQ-hjälp.',
|
||||
thai:
|
||||
'เป็นมนุษย์สุดประเสริฐเลิศคุณค่า กว่าบรรดาฝูงสัตว์เดรัจฉาน จงฝ่าฟันพัฒนาวิชาการ อย่าล้างผลาญฤๅเข่นฆ่าบีฑาใคร ไม่ถือโทษโกรธแช่งซัดฮึดฮัดด่า หัดอภัยเหมือนกีฬาอัชฌาสัย ปฏิบัติประพฤติกฎกำหนดใจ พูดจาให้จ๊ะๆ จ๋าๆ น่าฟังเอยฯ',
|
||||
};
|
||||
return (
|
||||
<View>
|
||||
<Picker
|
||||
selectedValue={this.state.language}
|
||||
onValueChange={itemValue => this.setState({language: itemValue})}>
|
||||
{Object.keys(PANGRAMS).map(x => (
|
||||
<Picker.Item
|
||||
label={x[0].toUpperCase() + x.substring(1)}
|
||||
key={x}
|
||||
value={x}
|
||||
/>
|
||||
))}
|
||||
</Picker>
|
||||
<View>
|
||||
{this.state.textMetrics.map(
|
||||
({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
capHeight,
|
||||
ascender,
|
||||
descender,
|
||||
xHeight,
|
||||
}) => {
|
||||
return [
|
||||
<View
|
||||
key="baseline view"
|
||||
style={{
|
||||
top: y + ascender,
|
||||
height: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
}}
|
||||
/>,
|
||||
<Text
|
||||
key="baseline text"
|
||||
style={{
|
||||
top: y + ascender,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
color: 'red',
|
||||
}}>
|
||||
Baseline
|
||||
</Text>,
|
||||
<View
|
||||
key="capheight view"
|
||||
style={{
|
||||
top: y + ascender - capHeight,
|
||||
height: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'green',
|
||||
}}
|
||||
/>,
|
||||
<Text
|
||||
key="capheight text"
|
||||
style={{
|
||||
top: y + ascender - capHeight,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
color: 'green',
|
||||
}}>
|
||||
Capheight
|
||||
</Text>,
|
||||
<View
|
||||
key="xheight view"
|
||||
style={{
|
||||
top: y + ascender - xHeight,
|
||||
height: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'blue',
|
||||
}}
|
||||
/>,
|
||||
<Text
|
||||
key="xheight text"
|
||||
style={{
|
||||
top: y + ascender - xHeight,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
color: 'blue',
|
||||
}}>
|
||||
X-height
|
||||
</Text>,
|
||||
<View
|
||||
key="descender view"
|
||||
style={{
|
||||
top: y + ascender + descender,
|
||||
height: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'orange',
|
||||
}}
|
||||
/>,
|
||||
<Text
|
||||
key="descender text"
|
||||
style={{
|
||||
top: y + ascender + descender,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
color: 'orange',
|
||||
}}>
|
||||
Descender
|
||||
</Text>,
|
||||
<View
|
||||
key="end of text view"
|
||||
style={{
|
||||
top: y,
|
||||
height: height,
|
||||
width: 1,
|
||||
left: x + width,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'brown',
|
||||
}}
|
||||
/>,
|
||||
<Text
|
||||
key="end of text text"
|
||||
style={{
|
||||
top: y,
|
||||
left: x + width + 5,
|
||||
position: 'absolute',
|
||||
color: 'brown',
|
||||
}}>
|
||||
End of text
|
||||
</Text>,
|
||||
];
|
||||
},
|
||||
)}
|
||||
<Text
|
||||
onTextLayout={event =>
|
||||
this.setState({textMetrics: event.nativeEvent.lines})
|
||||
}
|
||||
style={{fontSize: 50}}>
|
||||
{PANGRAMS[this.state.language]}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
exports.title = '<Text>';
|
||||
exports.description = 'Base component for rendering styled text.';
|
||||
exports.displayName = 'TextExample';
|
||||
|
@ -290,6 +603,24 @@ exports.examples = [
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Text metrics',
|
||||
render: function() {
|
||||
return <TextRenderInfoExample />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Text metrics legend',
|
||||
render: () => <TextLegend />,
|
||||
},
|
||||
{
|
||||
title: 'Baseline capheight box',
|
||||
render: () => (
|
||||
<View style={{backgroundColor: 'red'}}>
|
||||
<TextWithCapBaseBox>Some example text.</TextWithCapBaseBox>
|
||||
</View>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Padding',
|
||||
render: function() {
|
||||
|
|
Loading…
Reference in New Issue