Skip to content

Commit

Permalink
prototype(FastText): Add option to use TextBlock for simple text
Browse files Browse the repository at this point in the history
This is Eric Rozell's [PR](microsoft#1256) with a couple fixes to make it work with S4L. This optimization [gives](aka.ms/V3mz2) a 10% speedup and reduces memory usage by 10%.

Simple text, i.e., text with only a single raw text node as a child, can be transformed into a TextBlock with a Text property, instead of a RichTextBlock with a Paragraph of Inlines. This change detects in the React Native Text node has a single raw text child, and if so, uses the FastText native component. To test if your text nodes are "fast", set DebugSettings.IsTextPerformanceVisualizationEnabled to true while debugging your app; it changes optimized text color to green.

Related work items: #1164947
  • Loading branch information
antonkh committed May 3, 2018
1 parent 8d8289c commit 10c3493
Show file tree
Hide file tree
Showing 6 changed files with 418 additions and 6 deletions.
27 changes: 27 additions & 0 deletions Libraries/Text/Text.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ const viewConfig = {
uiViewClassName: 'RCTText',
};

const fastViewConfig = {
validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
numberOfLines: true,
lineBreakMode: true,
allowFontScaling: true,
selectable: true,
text: true,
}),
uiViewClassName: 'RCTFastText',
}

/**
* A React component for displaying text.
*
Expand Down Expand Up @@ -544,6 +556,19 @@ const Text = createReactClass({
if (this.context.isInAParentText) {
return <RCTVirtualText {...newProps} />;
} else {
if (Platform.OS === 'windows') {
let children = newProps.children;

if (typeof children === 'string' || Array.isArray(children) && children.every(x => typeof x === 'string')) {
let text = Array.isArray(children) ? children.join('\n') : children;
let fastProps = {};
Object.assign(fastProps, newProps);
fastProps.text = text;
delete fastProps.children;
return <RCTFastText {...fastProps} />;
}
}

return <RCTText {...newProps} />;
}
},
Expand All @@ -570,4 +595,6 @@ if (Platform.OS === 'android' || Platform.OS === 'windows') {
});
}

var RCTFastText = createReactNativeComponentClass(fastViewConfig);

module.exports = Text;
22 changes: 16 additions & 6 deletions ReactWindows/ReactNative.Shared/UIManager/RootViewHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Portions derived from React Native:
// Copyright (c) 2015-present, Facebook, Inc.
// Licensed under the MIT License.
Expand Down Expand Up @@ -104,12 +104,22 @@ private static DependencyObject GetParent(DependencyObject view, bool findRoot)

if (!findRoot)
{
return (view as FrameworkElement)?.Parent;
}
else
{
return VisualTreeHelper.GetParent(view);
var frameworkElement = view as FrameworkElement;
if (frameworkElement != null)
{
return frameworkElement.Parent;
}
#if !WINDOWS_UWP

var frameworkContentElement = view as FrameworkContentElement;
if (frameworkContentElement != null)
{
return frameworkContentElement.Parent;
}
#endif
}

return VisualTreeHelper.GetParent(view);
}
}
}
2 changes: 2 additions & 0 deletions ReactWindows/ReactNative/ReactNative.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@
<Compile Include="Views\Switch\ReactSwitchShadowNode.cs" />
<Compile Include="Views\TextInput\ReactPasswordBoxManager.cs" />
<Compile Include="Views\TextInput\ReactTextInputManager.cs" />
<Compile Include="Views\Text\ReactFastTextShadowNode.cs" />
<Compile Include="Views\Text\ReactFastTextViewManager.cs" />
<Compile Include="Views\Text\ReactTextCompoundView.cs" />
<Compile Include="Views\Text\ReactTextShadowNode.cs" />
<Compile Include="Views\Text\ReactTextViewManager.cs" />
Expand Down
1 change: 1 addition & 0 deletions ReactWindows/ReactNative/Shell/MainReactPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public IReadOnlyList<IViewManager> CreateViewManagers(
{
return new List<IViewManager>
{
new ReactFastTextViewManager(),
new ReactFlipViewManager(),
new ReactImageManager(),
new ReactProgressBarViewManager(),
Expand Down
281 changes: 281 additions & 0 deletions ReactWindows/ReactNative/Views/Text/ReactFastTextShadowNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
using Facebook.Yoga;
using ReactNative.Reflection;
using ReactNative.UIManager;
using ReactNative.UIManager.Annotations;
using System;
using Windows.Foundation;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace ReactNative.Views.Text
{
/// <summary>
/// The shadow node implementation for text views.
/// </summary>
public class ReactFastTextShadowNode : LayoutShadowNode
{
private int _letterSpacing;
private int _numberOfLines;

private double? _fontSize;
private double _lineHeight;

private bool _allowFontScaling;

private FontStyle? _fontStyle;
private FontWeight? _fontWeight;
private TextAlignment _textAlignment = TextAlignment.DetectFromContent;

private string _fontFamily;

private string _text;

/// <summary>
/// Instantiates a <see cref="ReactTextShadowNode"/>.
/// </summary>
public ReactFastTextShadowNode()
{
MeasureFunction = (node, width, widthMode, height, heightMode) =>
MeasureText(this, node, width, widthMode, height, heightMode);
}

/// <summary>
/// The view text.
/// </summary>
public string Text
{
get
{
return _text;
}
}

/// <summary>
/// Sets the text on the view.
/// </summary>
/// <param name="text">The text.</param>
[ReactProp("text")]
public void SetText(string text)
{
_text = text ?? "";
MarkUpdated();
}

/// <summary>
/// Sets the font size for the node.
/// </summary>
/// <param name="fontSize">The font size.</param>
[ReactProp(ViewProps.FontSize)]
public void SetFontSize(double? fontSize)
{
if (_fontSize != fontSize)
{
_fontSize = fontSize;
MarkUpdated();
}
}

/// <summary>
/// Sets the font family for the node.
/// </summary>
/// <param name="fontFamily">The font family.</param>
[ReactProp(ViewProps.FontFamily)]
public void SetFontFamily(string fontFamily)
{
if (_fontFamily != fontFamily)
{
_fontFamily = fontFamily;
MarkUpdated();
}
}

/// <summary>
/// Sets the font weight for the node.
/// </summary>
/// <param name="fontWeightValue">The font weight string.</param>
[ReactProp(ViewProps.FontWeight)]
public void SetFontWeight(string fontWeightValue)
{
var fontWeight = FontStyleHelpers.ParseFontWeight(fontWeightValue);
if (_fontWeight.HasValue != fontWeight.HasValue ||
(_fontWeight.HasValue && fontWeight.HasValue &&
_fontWeight.Value.Weight != fontWeight.Value.Weight))
{
_fontWeight = fontWeight;
MarkUpdated();
}
}

/// <summary>
/// Sets the font style for the node.
/// </summary>
/// <param name="fontStyleValue">The font style string.</param>
[ReactProp(ViewProps.FontStyle)]
public void SetFontStyle(string fontStyleValue)
{
var fontStyle = EnumHelpers.ParseNullable<FontStyle>(fontStyleValue);
if (_fontStyle != fontStyle)
{
_fontStyle = fontStyle;
MarkUpdated();
}
}

/// <summary>
/// Sets the letter spacing for the node.
/// </summary>
/// <param name="letterSpacing">The letter spacing.</param>
[ReactProp(ViewProps.LetterSpacing)]
public void SetLetterSpacing(int letterSpacing)
{
var spacing = 50 * letterSpacing; // TODO: Find exact multiplier (50) to match iOS

if (_letterSpacing != spacing)
{
_letterSpacing = spacing;
MarkUpdated();
}
}

/// <summary>
/// Sets the line height.
/// </summary>
/// <param name="lineHeight">The line height.</param>
[ReactProp(ViewProps.LineHeight)]
public virtual void SetLineHeight(double lineHeight)
{
if (_lineHeight != lineHeight)
{
_lineHeight = lineHeight;
MarkUpdated();
}
}

/// <summary>
/// Sets the maximum number of lines.
/// </summary>
/// <param name="numberOfLines">Max number of lines.</param>
[ReactProp(ViewProps.NumberOfLines)]
public virtual void SetNumberOfLines(int numberOfLines)
{
if (_numberOfLines != numberOfLines)
{
_numberOfLines = numberOfLines;
MarkUpdated();
}
}

/// <summary>
/// Sets the text alignment.
/// </summary>
/// <param name="textAlign">The text alignment string.</param>
[ReactProp(ViewProps.TextAlign)]
public void SetTextAlign(string textAlign)
{
var textAlignment = textAlign == "auto" || textAlign == null ?
TextAlignment.DetectFromContent :
EnumHelpers.Parse<TextAlignment>(textAlign);

if (_textAlignment != textAlignment)
{
_textAlignment = textAlignment;
MarkUpdated();
}
}

/// <summary>
/// Set fontScaling
/// </summary>
/// <param name="allowFontScaling">Max number of lines.</param>
[ReactProp(ViewProps.AllowFontScaling)]
public virtual void SetAllowFontScaling(bool allowFontScaling)
{
if (_allowFontScaling != allowFontScaling)
{
_allowFontScaling = allowFontScaling;
MarkUpdated();
}
}

/// <summary>
/// Called after a layout step at the end of a UI batch from
/// <see cref="UIManagerModule"/>. May be used to enqueue additional UI
/// operations for the native view. Will only be called on nodes marked
/// as updated.
/// </summary>
/// <param name="uiViewOperationQueue">
/// Interface for enqueueing UI operations.
/// </param>
public override void OnCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue)
{
base.OnCollectExtraUpdates(uiViewOperationQueue);
uiViewOperationQueue.EnqueueUpdateExtraData(ReactTag, this);
}

/// <summary>
/// Marks a node as updated.
/// </summary>
protected override void MarkUpdated()
{
base.MarkUpdated();
dirty();
}

private static YogaSize MeasureText(ReactFastTextShadowNode textNode, YogaNode node, float width, YogaMeasureMode widthMode, float height, YogaMeasureMode heightMode)
{
// TODO: Measure text with DirectWrite or other API that does not
// require dispatcher access. Currently, we're instantiating a
// second CoreApplicationView (that is never activated) and using
// its Dispatcher thread to calculate layout.
var textBlock = new TextBlock
{
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.DetectFromContent,
TextTrimming = TextTrimming.CharacterEllipsis,
};

textNode.UpdateTextBlockCore(textBlock, true);

var normalizedWidth = YogaConstants.IsUndefined(width) ? double.PositiveInfinity : width;
var normalizedHeight = YogaConstants.IsUndefined(height) ? double.PositiveInfinity : height;
textBlock.Measure(new Size(normalizedWidth, normalizedHeight));
return MeasureOutput.Make(
(float)Math.Ceiling(textBlock.DesiredSize.Width),
(float)Math.Ceiling(textBlock.DesiredSize.Height));
}

/// <summary>
/// Updates the properties of a <see cref="RichTextBlock"/> view.
/// </summary>
/// <param name="textBlock">The view.</param>
public void UpdateTextBlock(TextBlock textBlock)
{
UpdateTextBlockCore(textBlock, false);
}

private void UpdateTextBlockCore(TextBlock textBlock, bool measureOnly)
{
textBlock.CharacterSpacing = _letterSpacing;
textBlock.LineHeight = _lineHeight;
textBlock.MaxLines = _numberOfLines;
textBlock.TextAlignment = _textAlignment;
textBlock.FontFamily = _fontFamily != null ? new FontFamily(_fontFamily) : FontFamily.XamlAutoFontFamily;
textBlock.FontSize = _fontSize ?? 15;
textBlock.FontStyle = _fontStyle ?? FontStyle.Normal;
textBlock.FontWeight = _fontWeight ?? FontWeights.Normal;
textBlock.IsTextScaleFactorEnabled = _allowFontScaling;
textBlock.Text = _text;

if (!measureOnly)
{
textBlock.Padding = new Thickness(
GetPadding(YogaEdge.Left),
GetPadding(YogaEdge.Top),
0,
0);
}
}
}
}
Loading

0 comments on commit 10c3493

Please sign in to comment.