Add support for RCTText under FlatUIImplementation

Summary: @public Initial version of FlatUIImplementation lacks any primitives support (such as RCTText, RCTImageView or RCTView). This diff add the first part, RCTText (alongside with RCTVirtualText and RCTRawText).

Reviewed By: sriramramani

Differential Revision: D2693348
This commit is contained in:
Denis Koroskin 2015-12-06 16:00:36 -08:00 committed by Ahmed El-Helw
parent 44d2ee1c3f
commit 5c2f536e9a
21 changed files with 1466 additions and 20 deletions

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
/**
* Base class for all DrawCommands. Becomes immutable once it has its bounds set. Until then, a
* subclass is able to mutate any of its properties (e.g. updating Layout in DrawTextLayout).
*
* The idea is to be able to reuse unmodified objects when we build up DrawCommands before we ship
* them to UI thread, but we can only do that if DrawCommands are immutable.
*/
/* package */ abstract class AbstractDrawCommand implements DrawCommand, Cloneable {
private float mLeft;
private float mTop;
private float mRight;
private float mBottom;
private boolean mFrozen;
/**
* Updates boundaries of the AbstractDrawCommand and freezes it.
* Will return a frozen copy if the current AbstractDrawCommand cannot be mutated.
*/
public final AbstractDrawCommand updateBoundsAndFreeze(
float left,
float top,
float right,
float bottom) {
if (mFrozen) {
// see if we can reuse it
if (boundsMatch(left, top, right, bottom)) {
return this;
}
try {
AbstractDrawCommand copy = (AbstractDrawCommand) clone();
copy.setBounds(left, top, right, bottom);
return copy;
} catch (CloneNotSupportedException e) {
// This should not happen since AbstractDrawCommand implements Cloneable
throw new RuntimeException(e);
}
}
setBounds(left, top, right, bottom);
mFrozen = true;
return this;
}
/**
* Returns whether this object was frozen and thus cannot be mutated.
*/
public final boolean isFrozen() {
return mFrozen;
}
/**
* Left position of this DrawCommand relative to the hosting View.
*/
public final float getLeft() {
return mLeft;
}
/**
* Top position of this DrawCommand relative to the hosting View.
*/
public final float getTop() {
return mTop;
}
/**
* Right position of this DrawCommand relative to the hosting View.
*/
public final float getRight() {
return mRight;
}
/**
* Bottom position of this DrawCommand relative to the hosting View.
*/
public final float getBottom() {
return mBottom;
}
/**
* Updates boundaries of this DrawCommand.
*/
private void setBounds(float left, float top, float right, float bottom) {
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
}
/**
* Returns true if boundaries match and don't need to be updated. False otherwise.
*/
private boolean boundsMatch(float left, float top, float right, float bottom) {
return mLeft == left && mTop == top && mRight == right && mBottom == bottom;
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import android.graphics.Canvas;
/**
* DrawCommand is an inteface that shadow nodes need to implement to do the drawing.
* Instaces of DrawCommand are created in background thread and passed to UI thread.
* Once a DrawCommand is shared with UI thread, it can no longer be mutated in background thread.
*/
public interface DrawCommand {
// used by StateBuilder, FlatViewGroup and FlatShadowNode
/* package */ static final DrawCommand[] EMPTY_ARRAY = new DrawCommand[0];
/**
* Performs drawing into the given canvas.
*
* @param canvas The canvas to draw into
*/
public void draw(Canvas canvas);
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import android.graphics.Canvas;
import android.text.Layout;
/**
* DrawTextLayout is a DrawCommand that draw {@link Layout}.
*/
/* package */ final class DrawTextLayout extends AbstractDrawCommand {
private Layout mLayout;
/* package */ DrawTextLayout(Layout layout) {
mLayout = layout;
}
/**
* Assigns a new {@link Layout} to draw.
*/
public void setLayout(Layout layout) {
mLayout = layout;
}
@Override
public void draw(Canvas canvas) {
float left = getLeft();
float top = getTop();
canvas.translate(left, top);
mLayout.draw(canvas);
canvas.translate(-left, -top);
}
}

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import android.view.View;
import android.view.View.MeasureSpec;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.SizeMonitoringFrameLayout;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerRegistry;
/**
* FlatNativeViewHierarchyManager is the only class that performs View manipulations. All of this
* class methods can only be called from UI thread by {@link FlatUIViewOperationQueue}.
*/
/* package */ final class FlatNativeViewHierarchyManager extends NativeViewHierarchyManager {
/* package */ FlatNativeViewHierarchyManager(ViewManagerRegistry viewManagers) {
super(viewManagers);
}
@Override
public void addRootView(
int tag,
SizeMonitoringFrameLayout view,
ThemedReactContext themedContext) {
FlatViewGroup root = new FlatViewGroup(themedContext);
view.addView(root);
addRootViewGroup(tag, root, themedContext);
}
/**
* Assigns new DrawCommands to a FlatViewGroup specified by a reactTag.
*
* @param reactTag reactTag to lookup FlatViewGroup by
* @param drawCommands new draw commands to execute during the drawing.
*/
/* package */ void updateMountState(int reactTag, DrawCommand[] drawCommands) {
FlatViewGroup view = (FlatViewGroup) resolveView(reactTag);
view.mountDrawCommands(drawCommands);
}
/**
* Updates View bounds, possibly re-measuring and re-layouting it if the size changed.
*
* @param reactTag reactTag to lookup a View by
* @param left left coordinate relative to parent
* @param top top coordinate relative to parent
* @param right right coordinate relative to parent
* @param bottom bottom coordinate relative to parent
*/
/* package */ void updateViewBounds(int reactTag, int left, int top, int right, int bottom) {
View view = resolveView(reactTag);
int width = right - left;
int height = bottom - top;
if (view.getWidth() != width || view.getHeight() != height) {
// size changed, we need to measure and layout the View
view.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
view.layout(left, top, right, bottom);
} else {
// same size, only location changed, there is a faster route.
view.offsetLeftAndRight(left - view.getLeft());
view.offsetTopAndBottom(top - view.getTop());
}
}
}

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
/**
* Root node of the shadow node hierarchy. Currently, the only node that can actually map to a View.
*/
/* package */ final class FlatRootShadowNode extends FlatShadowNode {
private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
private int mViewLeft;
private int mViewTop;
private int mViewRight;
private int mViewBottom;
@Override
public int getScreenX() {
return mViewLeft;
}
@Override
public int getScreenY() {
return mViewTop;
}
@Override
public int getScreenWidth() {
return mViewRight - mViewLeft;
}
@Override
public int getScreenHeight() {
return mViewBottom - mViewTop;
}
/**
* Returns an array of DrawCommands to perform during the View's draw pass.
*/
/* package */ DrawCommand[] getDrawCommands() {
return mDrawCommands;
}
/**
* Sets an array of DrawCommands to perform during the View's draw pass. StateBuilder uses old
* draw commands to compare to new draw commands and see if the View neds to be redrawn.
*/
/* package */ void setDrawCommands(DrawCommand[] drawCommands) {
mDrawCommands = drawCommands;
}
/**
* Sets boundaries of the View that this node maps to relative to the parent left/top coordinate.
*/
/* package */ void setViewBounds(int left, int top, int right, int bottom) {
mViewLeft = left;
mViewTop = top;
mViewRight = right;
mViewBottom = bottom;
}
/**
* Left position of the View this node maps to relative to the parent View.
*/
/* package */ int getViewLeft() {
return mViewLeft;
}
/**
* Top position of the View this node maps to relative to the parent View.
*/
/* package */ int getViewTop() {
return mViewTop;
}
/**
* Right position of the View this node maps to relative to the parent View.
*/
/* package */ int getViewRight() {
return mViewRight;
}
/**
* Bottom position of the View this node maps to relative to the parent View.
*/
/* package */ int getViewBottom() {
return mViewBottom;
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import com.facebook.react.uimanager.LayoutShadowNode;
/**
* FlatShadowNode is a base class for all shadow node used in FlatUIImplementation. It extends
* {@link LayoutShadowNode} by adding an ability to prepare DrawCommands off the UI thread.
*/
/* package */ class FlatShadowNode extends LayoutShadowNode {
/**
* Collects DrawCommands produced by this FlatShadoNode.
*/
protected void collectState(
StateBuilder stateBuilder,
float left,
float top,
float right,
float bottom) {
// do nothing yet.
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import android.text.SpannableStringBuilder;
import com.facebook.react.uimanager.ReactShadowNode;
/**
* Base class for RCTVirtualText and RCTRawText.
*/
/* package */ abstract class FlatTextShadowNode extends FlatShadowNode {
/**
* Recursively visits FlatTextShadowNode and its children,
* appending text to SpannableStringBuilder.
*/
protected abstract void collectText(SpannableStringBuilder builder);
/**
* Recursively visits FlatTextShadowNode and its children,
* applying spans to SpannableStringBuilder.
*/
protected abstract void applySpans(SpannableStringBuilder builder);
/**
* Propagates changes up to RCTText without dirtying current node.
*/
protected void notifyChanged(boolean shouldRemeasure) {
ReactShadowNode parent = getParent();
if (parent instanceof FlatTextShadowNode) {
((FlatTextShadowNode) parent).notifyChanged(shouldRemeasure);
}
}
@Override
public boolean isVirtual() {
return true;
}
}

View File

@ -9,6 +9,7 @@
package com.facebook.react.flat;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
@ -33,20 +34,37 @@ import com.facebook.react.uimanager.events.EventDispatcher;
*/
public class FlatUIImplementation extends UIImplementation {
public FlatUIImplementation(
private final StateBuilder mStateBuilder;
public static FlatUIImplementation createInstance(
ReactApplicationContext reactContext,
List<ViewManager> viewManagers) {
this(reactContext, new ViewManagerRegistry(viewManagers));
viewManagers = new ArrayList<ViewManager>(viewManagers);
viewManagers.add(new RCTViewManager());
viewManagers.add(new RCTTextManager());
viewManagers.add(new RCTRawTextManager());
viewManagers.add(new RCTVirtualTextManager());
ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers);
FlatNativeViewHierarchyManager nativeViewHierarchyManager = new FlatNativeViewHierarchyManager(
viewManagerRegistry);
FlatUIViewOperationQueue operationsQueue = new FlatUIViewOperationQueue(
reactContext,
nativeViewHierarchyManager);
return new FlatUIImplementation(viewManagerRegistry, operationsQueue);
}
private FlatUIImplementation(
ReactApplicationContext reactContext,
ViewManagerRegistry viewManagers) {
super(
viewManagers,
new UIViewOperationQueue(
reactContext,
new NativeViewHierarchyManager(viewManagers)));
ViewManagerRegistry viewManagers,
FlatUIViewOperationQueue operationsQueue) {
super(viewManagers, operationsQueue);
mStateBuilder = new StateBuilder(operationsQueue);
}
@Override
protected ReactShadowNode createRootShadowNode() {
return new FlatRootShadowNode();
}
@Override
@ -122,16 +140,6 @@ public class FlatUIImplementation extends UIImplementation {
float absoluteX,
float absoluteY,
EventDispatcher eventDispatcher) {
markNodeLayoutSeen(cssNode);
}
private void markNodeLayoutSeen(CSSNode node) {
if (node.hasNewLayout()) {
node.markLayoutSeen();
}
for (int i = 0, childCount = node.getChildCount(); i != childCount; ++i) {
markNodeLayoutSeen(node.getChildAt(i));
}
mStateBuilder.applyUpdates((FlatRootShadowNode) cssNode);
}
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.UIViewOperationQueue;
/**
* FlatUIViewOperationQueue extends {@link UIViewOperationQueue} to add
* FlatUIImplementation-specific methods that need to run in UI thread.
*/
/* package */ final class FlatUIViewOperationQueue extends UIViewOperationQueue {
private final FlatNativeViewHierarchyManager mNativeViewHierarchyManager;
/**
* UIOperation that updates DrawCommands for a View defined by reactTag.
*/
private final class UpdateMountState implements UIOperation {
private final int mReactTag;
private final DrawCommand[] mDrawCommands;
private UpdateMountState(int reactTag, DrawCommand[] drawCommands) {
mReactTag = reactTag;
mDrawCommands = drawCommands;
}
@Override
public void execute() {
mNativeViewHierarchyManager.updateMountState(mReactTag, mDrawCommands);
}
}
/**
* UIOperation that updates View bounds for a View defined by reactTag.
*/
private final class UpdateViewBounds implements UIOperation {
private final int mReactTag;
private final int mLeft;
private final int mTop;
private final int mRight;
private final int mBottom;
private UpdateViewBounds(int reactTag, int left, int top, int right, int bottom) {
mReactTag = reactTag;
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
}
@Override
public void execute() {
mNativeViewHierarchyManager.updateViewBounds(mReactTag, mLeft, mTop, mRight, mBottom);
}
}
public FlatUIViewOperationQueue(
ReactApplicationContext reactContext,
FlatNativeViewHierarchyManager nativeViewHierarchyManager) {
super(reactContext, nativeViewHierarchyManager);
mNativeViewHierarchyManager = nativeViewHierarchyManager;
}
/**
* Enqueues a new UIOperation that will update DrawCommands for a View defined by reactTag.
*/
public void enqueueUpdateMountState(int reactTag, DrawCommand[] drawCommands) {
enqueueUIOperation(new UpdateMountState(reactTag, drawCommands));
}
/**
* Enqueues a new UIOperation that will update View bounds for a View defined by reactTag.
*/
public void enqueueUpdateViewBounds(int reactTag, int left, int top, int right, int bottom) {
enqueueUIOperation(new UpdateViewBounds(reactTag, left, top, right, bottom));
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import android.content.Context;
import android.graphics.Canvas;
import android.view.ViewGroup;
/**
* A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over
* array of DrawCommands, executing them one by one.
*/
/* package */ final class FlatViewGroup extends ViewGroup {
private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
/* package */ FlatViewGroup(Context context) {
super(context);
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
for (DrawCommand drawCommand : mDrawCommands) {
drawCommand.draw(canvas);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// nothing to do here
}
/* package */ void mountDrawCommands(DrawCommand[] drawCommands) {
mDrawCommands = drawCommands;
invalidate();
}
}

View File

@ -0,0 +1,99 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import javax.annotation.Nullable;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
/* package */ final class FontStylingSpan extends MetricAffectingSpan {
// text property
private final double mTextColor;
private final int mBackgroundColor;
// font properties
private final int mFontSize;
private final int mFontStyle;
private final int mFontWeight;
private final @Nullable String mFontFamily;
FontStylingSpan(
double textColor,
int backgroundColor,
int fontSize,
int fontStyle,
int fontWeight,
@Nullable String fontFamily) {
mTextColor = textColor;
mBackgroundColor = backgroundColor;
mFontSize = fontSize;
mFontStyle = fontStyle;
mFontWeight = fontWeight;
mFontFamily = fontFamily;
}
@Override
public void updateDrawState(TextPaint ds) {
if (!Double.isNaN(mTextColor)) {
ds.setColor((int) mTextColor);
}
ds.bgColor = mBackgroundColor;
updateMeasureState(ds);
}
@Override
public void updateMeasureState(TextPaint ds) {
if (mFontSize != -1) {
ds.setTextSize(mFontSize);
}
updateTypeface(ds);
}
private int getNewStyle(int oldStyle) {
int newStyle = oldStyle;
if (mFontStyle != -1) {
newStyle = (newStyle & ~Typeface.ITALIC) | mFontStyle;
}
if (mFontWeight != -1) {
newStyle = (newStyle & ~Typeface.BOLD) | mFontWeight;
}
return newStyle;
}
private void updateTypeface(TextPaint ds) {
Typeface typeface = ds.getTypeface();
int oldStyle = (typeface == null) ? 0 : typeface.getStyle();
int newStyle = getNewStyle(oldStyle);
if (oldStyle == newStyle && mFontFamily == null) {
// nothing to do
return;
}
// TODO: optimize this part (implemented in a followup patch)
if (mFontFamily != null) {
// efficient in API 21+
typeface = Typeface.create(mFontFamily, newStyle);
} else {
// efficient in API 16+
typeface = Typeface.create(typeface, newStyle);
}
ds.setTypeface(typeface);
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import javax.annotation.Nullable;
import android.text.SpannableStringBuilder;
import com.facebook.react.uimanager.ReactProp;
/**
* RCTRawText is a FlatTextShadowNode that can only contain raw text (but not styling).
*/
/* package */ class RCTRawText extends FlatTextShadowNode {
private @Nullable String mText;
@Override
protected void collectText(SpannableStringBuilder builder) {
if (mText != null) {
builder.append(mText);
}
// RCTRawText cannot have any children, so no recursive calls needed.
}
@Override
protected void applySpans(SpannableStringBuilder builder) {
// no spans and no children so nothing to do here.
}
@ReactProp(name = "text")
public void setText(@Nullable String text) {
mText = text;
notifyChanged(true);
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
/**
* ViewManager that creates instances of RCTRawText.
*/
/* package */ final class RCTRawTextManager extends VirtualViewManager<RCTRawText> {
@Override
public String getName() {
return "RCTRawText";
}
@Override
public RCTRawText createShadowNodeInstance() {
return new RCTRawText();
}
@Override
public Class<RCTRawText> getShadowNodeClass() {
return RCTRawText.class;
}
}

View File

@ -0,0 +1,209 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import javax.annotation.Nullable;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import com.facebook.csslayout.CSSNode;
import com.facebook.csslayout.MeasureOutput;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactProp;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.ViewProps;
/**
* RCTText is a top-level node for text. It extends {@link RCTVirtualText} because it can contain
* styling information, but has the following differences:
*
* a) RCTText is not a virtual node, and can be measured and laid out.
* b) when no font size is specified, a font size of ViewDefaults#FONT_SIZE_SP is assumed.
*/
/* package */ final class RCTText extends RCTVirtualText implements CSSNode.MeasureFunction {
private static final boolean INCLUDE_PADDING = true;
private static final TextPaint PAINT = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
// this is optional, and helps saving a few BoringLayout.Metrics allocations during measure().
private static @Nullable BoringLayout.Metrics sBoringLayoutMetrics;
private @Nullable CharSequence mText;
private @Nullable DrawTextLayout mDrawCommand;
private @Nullable BoringLayout.Metrics mBoringLayoutMetrics;
private float mSpacingMult = 1.0f;
private float mSpacingAdd = 0.0f;
public RCTText() {
setMeasureFunction(this);
}
@Override
public boolean isVirtual() {
return false;
}
@Override
public boolean isVirtualAnchor() {
return true;
}
@Override
public void measure(CSSNode node, float width, MeasureOutput measureOutput) {
CharSequence text = getText();
if (TextUtils.isEmpty(text)) {
// to indicate that we don't have anything to display
mText = null;
measureOutput.width = 0;
measureOutput.height = 0;
return;
}
mText = text;
BoringLayout.Metrics metrics = BoringLayout.isBoring(text, PAINT, sBoringLayoutMetrics);
if (metrics != null) {
sBoringLayoutMetrics = mBoringLayoutMetrics;
if (sBoringLayoutMetrics != null) {
// make sure it's always empty, reported metrics can be incorrect otherwise
sBoringLayoutMetrics.top = 0;
sBoringLayoutMetrics.ascent = 0;
sBoringLayoutMetrics.descent = 0;
sBoringLayoutMetrics.bottom = 0;
sBoringLayoutMetrics.leading = 0;
}
mBoringLayoutMetrics = metrics;
float measuredWidth = (float) metrics.width;
if (Float.isNaN(width) || measuredWidth <= width) {
measureOutput.width = measuredWidth;
measureOutput.height = getMetricsHeight(metrics, INCLUDE_PADDING);
// to indicate that text layout was not created during the measure pass
mDrawCommand = null;
return;
}
// width < measuredWidth -> more that a single line -> not boring
}
int maximumWidth = Float.isNaN(width) ? Integer.MAX_VALUE : (int) width;
// at this point we need to create a StaticLayout to measure the text
StaticLayout layout = new StaticLayout(
text,
PAINT,
maximumWidth,
Layout.Alignment.ALIGN_NORMAL,
mSpacingMult,
mSpacingAdd,
INCLUDE_PADDING);
// determine how wide we actually are
float maxLineWidth = 0;
int lineCount = layout.getLineCount();
for (int i = 0; i != lineCount; ++i) {
maxLineWidth = Math.max(maxLineWidth, layout.getLineMax(i));
}
measureOutput.width = maxLineWidth;
measureOutput.height = layout.getHeight();
if (mDrawCommand != null && !mDrawCommand.isFrozen()) {
mDrawCommand.setLayout(layout);
} else {
mDrawCommand = new DrawTextLayout(layout);
}
}
@Override
protected void collectState(
StateBuilder stateBuilder,
float left,
float top,
float right,
float bottom) {
super.collectState(stateBuilder, left, top, right, bottom);
if (mText == null) {
// nothing to draw (empty text).
return;
}
if (mDrawCommand == null) {
// Layout was not created during the measure pass, must be Boring, create it now
mDrawCommand = new DrawTextLayout(new BoringLayout(
mText,
PAINT,
Integer.MAX_VALUE, // fits one line so don't care about the width
Layout.Alignment.ALIGN_NORMAL,
mSpacingMult,
mSpacingAdd,
mBoringLayoutMetrics,
INCLUDE_PADDING));
}
mDrawCommand = (DrawTextLayout) mDrawCommand.updateBoundsAndFreeze(left, top, right, bottom);
stateBuilder.addDrawCommand(mDrawCommand);
}
@ReactProp(name = ViewProps.LINE_HEIGHT, defaultDouble = Double.NaN)
public void setLineHeight(double lineHeight) {
if (Double.isNaN(lineHeight)) {
mSpacingMult = 1.0f;
mSpacingAdd = 0.0f;
} else {
mSpacingMult = 0.0f;
mSpacingAdd = PixelUtil.toPixelFromSP((float) lineHeight);
}
notifyChanged(true);
}
@Override
protected int getDefaultFontSize() {
// top-level <Text /> should always specify font size.
return fontSizeFromSp(ViewDefaults.FONT_SIZE_SP);
}
@Override
protected void notifyChanged(boolean shouldRemeasure) {
// Future patch: should only recreate Layout if shouldRemeasure is false
dirty();
}
/**
* Returns a new CharSequence that includes all the text and styling information to create Layout.
*/
private CharSequence getText() {
SpannableStringBuilder sb = new SpannableStringBuilder();
collectText(sb);
applySpans(sb);
return sb;
}
/**
* Returns measured line height according to an includePadding flag.
*/
private static int getMetricsHeight(BoringLayout.Metrics metrics, boolean includePadding) {
if (includePadding) {
return metrics.bottom - metrics.top;
} else {
return metrics.descent - metrics.ascent;
}
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
/**
* ViewManager that creates instances of RCTText.
*/
/* package */ final class RCTTextManager extends VirtualViewManager<RCTText> {
@Override
public String getName() {
return "RCTText";
}
@Override
public RCTText createShadowNodeInstance() {
return new RCTText();
}
@Override
public Class<RCTText> getShadowNodeClass() {
return RCTText.class;
}
}

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
/**
* Dummy implementation of RCTView.
*/
/* package */ final class RCTView extends FlatShadowNode {
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
/**
* ViewManager that creates instances of RCTView.
*/
/* package */ final class RCTViewManager extends VirtualViewManager<RCTView> {
@Override
public String getName() {
return "RCTView";
}
@Override
public RCTView createShadowNodeInstance() {
return new RCTView();
}
@Override
public Class<RCTView> getShadowNodeClass() {
return RCTView.class;
}
}

View File

@ -0,0 +1,174 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import javax.annotation.Nullable;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactProp;
import com.facebook.react.uimanager.ViewProps;
/**
* RCTVirtualText is a {@link FlatTextShadowNode} that can contain font styling information.
*/
/* package */ class RCTVirtualText extends FlatTextShadowNode {
private static final String BOLD = "bold";
private static final String ITALIC = "italic";
private static final String NORMAL = "normal";
// TODO: cache CustomStyleSpan and move remove these values from here
// (implemented in a followup patch)
private double mTextColor = Double.NaN;
private int mBgColor;
private int mFontSize = getDefaultFontSize();
private int mFontStyle = -1; // -1, Typeface.NORMAL or Typeface.ITALIC
private int mFontWeight = -1; // -1, Typeface.NORMAL or Typeface.BOLD
private @Nullable String mFontFamily;
// these 2 are only used between collectText() and applySpans() calls.
private int mTextBegin;
private int mTextEnd;
@Override
protected void collectText(SpannableStringBuilder builder) {
int childCount = getChildCount();
mTextBegin = builder.length();
for (int i = 0; i < childCount; ++i) {
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
child.collectText(builder);
}
mTextEnd = builder.length();
}
@Override
protected void applySpans(SpannableStringBuilder builder) {
if (mTextBegin == mTextEnd) {
return;
}
builder.setSpan(
// Future patch: cache last custom style span with a frozen flag
new FontStylingSpan(mTextColor, mBgColor, mFontSize, mFontStyle, mFontWeight, mFontFamily),
mTextBegin,
mTextEnd,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
int childCount = getChildCount();
for (int i = 0; i < childCount; ++i) {
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
child.applySpans(builder);
}
}
@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = Float.NaN)
public void setFontSize(float fontSizeSp) {
final int fontSize;
if (Float.isNaN(fontSizeSp)) {
fontSize = getDefaultFontSize();
} else {
fontSize = fontSizeFromSp(fontSizeSp);
}
if (mFontSize != fontSize) {
mFontSize = fontSize;
notifyChanged(true);
}
}
@ReactProp(name = ViewProps.COLOR, defaultDouble = Double.NaN)
public void setColor(double textColor) {
if (mTextColor != textColor) {
mTextColor = textColor;
notifyChanged(false);
}
}
@ReactProp(name = ViewProps.BACKGROUND_COLOR)
public void setBackgroundColor(int backgroundColor) {
if (mBgColor != backgroundColor) {
mBgColor = backgroundColor;
notifyChanged(false);
}
}
@ReactProp(name = ViewProps.FONT_FAMILY)
public void setFontFamily(@Nullable String fontFamily) {
mFontFamily = fontFamily;
notifyChanged(true);
}
@ReactProp(name = ViewProps.FONT_WEIGHT)
public void setFontWeight(@Nullable String fontWeightString) {
final int fontWeight;
if (fontWeightString == null) {
fontWeight = -1;
} else if (BOLD.equals(fontWeightString)) {
fontWeight = Typeface.BOLD;
} else if (NORMAL.equals(fontWeightString)) {
fontWeight = Typeface.NORMAL;
} else {
int fontWeightNumeric = parseNumericFontWeight(fontWeightString);
if (fontWeightNumeric == -1) {
throw new RuntimeException("invalid font weight " + fontWeightString);
}
fontWeight = fontWeightNumeric >= 500 ? Typeface.BOLD : Typeface.NORMAL;
}
if (mFontWeight != fontWeight) {
mFontWeight = fontWeight;
notifyChanged(true);
}
}
@ReactProp(name = ViewProps.FONT_STYLE)
public void setFontStyle(@Nullable String fontStyleString) {
final int fontStyle;
if (fontStyleString == null) {
fontStyle = -1;
} else if (ITALIC.equals(fontStyleString)) {
fontStyle = Typeface.ITALIC;
} else if (NORMAL.equals(fontStyleString)) {
fontStyle = Typeface.NORMAL;
} else {
throw new RuntimeException("invalid font style " + fontStyleString);
}
if (mFontStyle != fontStyle) {
mFontStyle = fontStyle;
notifyChanged(true);
}
}
protected int getDefaultFontSize() {
return -1;
}
/* package */ static int fontSizeFromSp(float sp) {
return (int) Math.ceil(PixelUtil.toPixelFromSP(sp));
}
/**
* Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
* return the weight.
*/
private static int parseNumericFontWeight(String fontWeightString) {
// This should be much faster than using regex to verify input and Integer.parseInt
return fontWeightString.length() == 3 && fontWeightString.endsWith("00")
&& fontWeightString.charAt(0) <= '9' && fontWeightString.charAt(0) >= '1' ?
100 * (fontWeightString.charAt(0) - '0') : -1;
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
/**
* ViewManager that creates instances of RCTVirtualText.
*/
/* package */ final class RCTVirtualTextManager extends VirtualViewManager<RCTVirtualText> {
@Override
public String getName() {
return "RCTVirtualText";
}
@Override
public RCTVirtualText createShadowNodeInstance() {
return new RCTVirtualText();
}
@Override
public Class<RCTVirtualText> getShadowNodeClass() {
return RCTVirtualText.class;
}
}

View File

@ -0,0 +1,179 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import java.util.ArrayDeque;
/**
* Shadow node hierarchy by itself cannot display UI, it is only a representation of what UI should
* be from JavaScript perspective. StateBuilder is a helper class that can walk the shadow node tree
* and collect information that can then be passed to UI thread and applied to a hierarchy of Views
* that Android finally can display.
*/
/* package */ final class StateBuilder {
private final FlatUIViewOperationQueue mOperationsQueue;
// DrawCommands
private final ArrayDeque<DrawCommand> mDrawCommands = new ArrayDeque<>();
private DrawCommand[] mPreviousDrawCommands = DrawCommand.EMPTY_ARRAY;
private int mPreviousDrawCommandsIndex;
/* package */ StateBuilder(FlatUIViewOperationQueue operationsQueue) {
mOperationsQueue = operationsQueue;
}
/**
* Given a root of the laid-out shadow node hierarchy, walks the tree and generates an array of
* DrawCommands that will then mount in UI thread to a root FlatViewGroup so that it can draw.
*/
/* package*/ void applyUpdates(FlatRootShadowNode node) {
collectStateAndUpdateViewBounds(node, 0, 0);
}
/**
* Adds a DrawCommand for current mountable node.
*/
/* package */ void addDrawCommand(AbstractDrawCommand drawCommand) {
if (mPreviousDrawCommandsIndex < mPreviousDrawCommands.length &&
mPreviousDrawCommands[mPreviousDrawCommandsIndex] == drawCommand) {
++mPreviousDrawCommandsIndex;
} else {
mPreviousDrawCommandsIndex = mPreviousDrawCommands.length + 1;
}
mDrawCommands.addLast(drawCommand);
}
/**
* Updates boundaries of a View that a give nodes maps to.
*/
private void updateViewBounds(
FlatRootShadowNode node,
int tag,
float leftInParent,
float topInParent,
float rightInParent,
float bottomInParent) {
int viewLeft = Math.round(leftInParent);
int viewTop = Math.round(topInParent);
int viewRight = Math.round(rightInParent);
int viewBottom = Math.round(bottomInParent);
if (node.getViewLeft() == viewLeft && node.getViewTop() == viewTop &&
node.getViewRight() == viewRight && node.getViewBottom() == viewBottom) {
// nothing changed.
return;
}
// this will optionally measure and layout the View this node maps to.
node.setViewBounds(viewLeft, viewTop, viewRight, viewBottom);
mOperationsQueue.enqueueUpdateViewBounds(tag, viewLeft, viewTop, viewRight, viewBottom);
}
/**
* Collects state (DrawCommands) for a given node that will mount to a View.
*/
private void collectStateForMountableNode(
FlatRootShadowNode node,
int tag,
float width,
float height) {
// save
int d = mDrawCommands.size();
DrawCommand[] previousDrawCommands = mPreviousDrawCommands;
int previousDrawCommandsIndex = mPreviousDrawCommandsIndex;
// reset
mPreviousDrawCommands = node.getDrawCommands();
mPreviousDrawCommandsIndex = 0;
collectStateRecursively(node, 0, 0, width, height);
if (mPreviousDrawCommandsIndex != mPreviousDrawCommands.length) {
// DrawCommands changes, need to re-mount them and re-draw the View.
DrawCommand[] drawCommands = extractDrawCommands(d);
node.setDrawCommands(drawCommands);
mOperationsQueue.enqueueUpdateMountState(tag, drawCommands);
}
// restore
mPreviousDrawCommandsIndex = previousDrawCommandsIndex;
mPreviousDrawCommands = previousDrawCommands;
}
/**
* Returns all DrawCommands collectes so far starting from a given index.
*/
private DrawCommand[] extractDrawCommands(int lowerBound) {
int upperBound = mDrawCommands.size();
int size = upperBound - lowerBound;
if (size == 0) {
// avoid allocating empty array
return DrawCommand.EMPTY_ARRAY;
}
DrawCommand[] drawCommands = new DrawCommand[size];
for (int i = 0; i < size; ++i) {
drawCommands[i] = mDrawCommands.pollFirst();
}
return drawCommands;
}
/**
* Recursively walks node tree from a given node and collects DrawCommands.
*/
private void collectStateRecursively(
FlatShadowNode node,
float left,
float top,
float right,
float bottom) {
if (node.hasNewLayout()) {
node.markLayoutSeen();
}
node.collectState(this, left, top, right, bottom);
for (int i = 0, childCount = node.getChildCount(); i != childCount; ++i) {
FlatShadowNode child = (FlatShadowNode) node.getChildAt(i);
float childLeft = left + child.getLayoutX();
float childTop = top + child.getLayoutY();
float childRight = childLeft + child.getLayoutWidth();
float childBottom = childTop + child.getLayoutHeight();
collectStateRecursively(child, childLeft, childTop, childRight, childBottom);
}
}
/**
* Collects state and updates View boundaries for a given root node.
*/
private void collectStateAndUpdateViewBounds(
FlatRootShadowNode node,
float parentLeft,
float parentTop) {
int tag = node.getReactTag();
float width = node.getLayoutWidth();
float height = node.getLayoutHeight();
float left = parentLeft + node.getLayoutX();
float top = parentTop + node.getLayoutY();
float right = left + width;
float bottom = top + height;
collectStateForMountableNode(node, tag, width, height);
updateViewBounds(node, tag, left, top, right, bottom);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import android.view.View;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManager;
/**
* Base class to ViewManagers that don't map to a View.
*/
abstract class VirtualViewManager<C extends FlatShadowNode> extends ViewManager<View, C> {
@Override
protected View createViewInstance(ThemedReactContext reactContext) {
throw new RuntimeException(getName() + " doesn't map to a View");
}
@Override
public void updateExtraData(View root, Object extraData) {
}
}