mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-03-06 22:37:14 +08:00
Android: Enable views to be nested within <Text> (#23195)
Summary: Potential breaking change: The signature of ReactShadowNode's onBeforeLayout method was changed - Before: public void onBeforeLayout() - After: public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) Implements same feature as this iOS PR: https://github.com/facebook/react-native/pull/7304 Previously, only Text and Image could be nested within Text. Now, any view can be nested within Text. One restriction of this feature is that developers must give inline views a width and a height via the style prop. Previously, inline Images were supported via FrescoBasedReactTextInlineImageSpan. To get support for nesting views within Text, we create one special kind of span per inline view. This span is called TextInlineViewPlaceholderSpan. It is the same size as the inline view. Its job is just to occupy space -- it doesn't render any visual. After the text is rendered, we query the Android Layout object associated with the TextView to find out where it has positioned each TextInlineViewPlaceholderSpan. We then position the views to be at those locations. One tricky aspect of the implementation is that the Text component needs to be able to render native children (the inline views) but the Android TextView cannot have children. This is solved by having the native parent of the ReactTextView also host the inline views. Implementation-wise, this was accomplished by extending the NativeViewHierarchyOptimizer to handle this case. The optimizer now handles these cases: - Node is not in the native tree. An ancestor must host its children. - Node is in the native tree and it can host its own children. - (new) Node is in the native tree but it cannot host its own children. An ancestor must host both this node and its children. I added the `onInlineViewLayout` event which is useful for writing tests for verifying that the inline views are positioned properly. Limitation: Clipping ---------- If Text's height/width is small such that an inline view doesn't completely fit, the inline view may still be fully visible due to hoisting (the inline view isn't actually parented to the Text which has the limited size. It is parented to an ancestor which may have a different clipping rectangle.). Prior to this change, layout-only views had a similar limitation. Pull Request resolved: https://github.com/facebook/react-native/pull/23195 Differential Revision: D14014668 Pulled By: shergin fbshipit-source-id: d46130f3d19cc83ac7ddf423adcc9e23988245d3
This commit is contained in:
committed by
Facebook Github Bot
parent
770da3ac67
commit
a2285b1790
@@ -11,11 +11,8 @@
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const TextAncestor = require('TextAncestor');
|
||||
const ViewNativeComponent = require('ViewNativeComponent');
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
import type {ViewProps} from 'ViewPropTypes';
|
||||
|
||||
export type Props = ViewProps;
|
||||
@@ -35,17 +32,7 @@ if (__DEV__) {
|
||||
props: Props,
|
||||
forwardedRef: React.Ref<typeof ViewNativeComponent>,
|
||||
) => {
|
||||
return (
|
||||
<TextAncestor.Consumer>
|
||||
{hasTextAncestor => {
|
||||
invariant(
|
||||
!hasTextAncestor,
|
||||
'Nesting of <View> within <Text> is not currently supported.',
|
||||
);
|
||||
return <ViewNativeComponent {...props} ref={forwardedRef} />;
|
||||
}}
|
||||
</TextAncestor.Consumer>
|
||||
);
|
||||
return <ViewNativeComponent {...props} ref={forwardedRef} />;
|
||||
};
|
||||
ViewToExport = React.forwardRef(View);
|
||||
ViewToExport.displayName = 'View';
|
||||
|
||||
@@ -66,12 +66,16 @@ const viewConfig = {
|
||||
minimumFontScale: true,
|
||||
textBreakStrategy: true,
|
||||
onTextLayout: true,
|
||||
onInlineViewLayout: true,
|
||||
dataDetectorType: true,
|
||||
},
|
||||
directEventTypes: {
|
||||
topTextLayout: {
|
||||
registrationName: 'onTextLayout',
|
||||
},
|
||||
topInlineViewLayout: {
|
||||
registrationName: 'onInlineViewLayout',
|
||||
},
|
||||
},
|
||||
uiViewClassName: 'RCTText',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
public interface IViewManagerWithChildren {
|
||||
/**
|
||||
* Returns whether this View type needs to handle laying out its own children instead of
|
||||
* deferring to the standard css-layout algorithm.
|
||||
* Returns true for the layout to *not* be automatically invoked. Instead onLayout will be
|
||||
* invoked as normal and it is the View instance's responsibility to properly call layout on its
|
||||
* children.
|
||||
* Returns false for the default behavior of automatically laying out children without going
|
||||
* through the ViewGroup's onLayout method. In that case, onLayout for this View type must *not*
|
||||
* call layout on its children.
|
||||
*/
|
||||
public boolean needsCustomLayoutForChildren();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
// Common conditionals:
|
||||
// - `kind == PARENT` checks whether the node can host children in the native tree.
|
||||
// - `kind != NONE` checks whether the node appears in the native tree.
|
||||
|
||||
public enum NativeKind {
|
||||
// Node is in the native hierarchy and the HierarchyOptimizer should assume it can host children
|
||||
// (e.g. because it's a ViewGroup). Note that it's okay if the node doesn't support children. When
|
||||
// the HierarchyOptimizer generates children manipulation commands for that node, the
|
||||
// HierarchyManager will catch this case and throw an exception.
|
||||
PARENT,
|
||||
// Node is in the native hierarchy, it may have children, but it cannot host them itself (e.g.
|
||||
// because it isn't a ViewGroup). Consequently, its children need to be hosted by an ancestor.
|
||||
LEAF,
|
||||
// Node is not in the native hierarchy.
|
||||
NONE
|
||||
}
|
||||
@@ -195,16 +195,16 @@ public class NativeViewHierarchyManager {
|
||||
// Check if the parent of the view has to layout the view, or the child has to lay itself out.
|
||||
if (!mRootTags.get(parentTag)) {
|
||||
ViewManager parentViewManager = mTagsToViewManagers.get(parentTag);
|
||||
ViewGroupManager parentViewGroupManager;
|
||||
if (parentViewManager instanceof ViewGroupManager) {
|
||||
parentViewGroupManager = (ViewGroupManager) parentViewManager;
|
||||
IViewManagerWithChildren parentViewManagerWithChildren;
|
||||
if (parentViewManager instanceof IViewManagerWithChildren) {
|
||||
parentViewManagerWithChildren = (IViewManagerWithChildren) parentViewManager;
|
||||
} else {
|
||||
throw new IllegalViewOperationException(
|
||||
"Trying to use view with tag " + parentTag +
|
||||
" as a parent, but its Manager doesn't extends ViewGroupManager");
|
||||
" as a parent, but its Manager doesn't implement IViewManagerWithChildren");
|
||||
}
|
||||
if (parentViewGroupManager != null
|
||||
&& !parentViewGroupManager.needsCustomLayoutForChildren()) {
|
||||
if (parentViewManagerWithChildren != null
|
||||
&& !parentViewManagerWithChildren.needsCustomLayoutForChildren()) {
|
||||
updateLayout(viewToUpdate, x, y, width, height);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -64,6 +64,15 @@ public class NativeViewHierarchyOptimizer {
|
||||
private final ShadowNodeRegistry mShadowNodeRegistry;
|
||||
private final SparseBooleanArray mTagsWithLayoutVisited = new SparseBooleanArray();
|
||||
|
||||
public static void assertNodeSupportedWithoutOptimizer(ReactShadowNode node) {
|
||||
// NativeKind.LEAF nodes require the optimizer. They are not ViewGroups so they cannot host
|
||||
// their native children themselves. Their native children need to be hoisted by the optimizer
|
||||
// to an ancestor which is a ViewGroup.
|
||||
Assertions.assertCondition(
|
||||
node.getNativeKind() != NativeKind.LEAF,
|
||||
"Nodes with NativeKind.LEAF are not supported when the optimizer is disabled");
|
||||
}
|
||||
|
||||
public NativeViewHierarchyOptimizer(
|
||||
UIViewOperationQueue uiViewOperationQueue,
|
||||
ShadowNodeRegistry shadowNodeRegistry) {
|
||||
@@ -79,6 +88,7 @@ public class NativeViewHierarchyOptimizer {
|
||||
ThemedReactContext themedContext,
|
||||
@Nullable ReactStylesDiffMap initialProps) {
|
||||
if (!ENABLED) {
|
||||
assertNodeSupportedWithoutOptimizer(node);
|
||||
int tag = node.getReactTag();
|
||||
mUIViewOperationQueue.enqueueCreateView(
|
||||
themedContext,
|
||||
@@ -92,7 +102,7 @@ public class NativeViewHierarchyOptimizer {
|
||||
isLayoutOnlyAndCollapsable(initialProps);
|
||||
node.setIsLayoutOnly(isLayoutOnly);
|
||||
|
||||
if (!isLayoutOnly) {
|
||||
if (node.getNativeKind() != NativeKind.NONE) {
|
||||
mUIViewOperationQueue.enqueueCreateView(
|
||||
themedContext,
|
||||
node.getReactTag(),
|
||||
@@ -118,6 +128,7 @@ public class NativeViewHierarchyOptimizer {
|
||||
String className,
|
||||
ReactStylesDiffMap props) {
|
||||
if (!ENABLED) {
|
||||
assertNodeSupportedWithoutOptimizer(node);
|
||||
mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
|
||||
return;
|
||||
}
|
||||
@@ -148,6 +159,7 @@ public class NativeViewHierarchyOptimizer {
|
||||
int[] tagsToDelete,
|
||||
int[] indicesToDelete) {
|
||||
if (!ENABLED) {
|
||||
assertNodeSupportedWithoutOptimizer(nodeToManage);
|
||||
mUIViewOperationQueue.enqueueManageChildren(
|
||||
nodeToManage.getReactTag(),
|
||||
indicesToRemove,
|
||||
@@ -189,6 +201,7 @@ public class NativeViewHierarchyOptimizer {
|
||||
ReadableArray childrenTags
|
||||
) {
|
||||
if (!ENABLED) {
|
||||
assertNodeSupportedWithoutOptimizer(nodeToManage);
|
||||
mUIViewOperationQueue.enqueueSetChildren(
|
||||
nodeToManage.getReactTag(),
|
||||
childrenTags);
|
||||
@@ -208,8 +221,9 @@ public class NativeViewHierarchyOptimizer {
|
||||
*/
|
||||
public void handleUpdateLayout(ReactShadowNode node) {
|
||||
if (!ENABLED) {
|
||||
assertNodeSupportedWithoutOptimizer(node);
|
||||
mUIViewOperationQueue.enqueueUpdateLayout(
|
||||
Assertions.assertNotNull(node.getParent()).getReactTag(),
|
||||
Assertions.assertNotNull(node.getLayoutParent()).getReactTag(),
|
||||
node.getReactTag(),
|
||||
node.getScreenX(),
|
||||
node.getScreenY(),
|
||||
@@ -221,6 +235,12 @@ public class NativeViewHierarchyOptimizer {
|
||||
applyLayoutBase(node);
|
||||
}
|
||||
|
||||
public void handleForceViewToBeNonLayoutOnly(ReactShadowNode node) {
|
||||
if (node.isLayoutOnly()) {
|
||||
transitionLayoutOnlyViewToNativeView(node, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the shadow hierarchy to dispatch all necessary updateLayout calls to the native
|
||||
* hierarchy. Should be called after all updateLayout calls for a batch have been handled.
|
||||
@@ -229,16 +249,18 @@ public class NativeViewHierarchyOptimizer {
|
||||
mTagsWithLayoutVisited.clear();
|
||||
}
|
||||
|
||||
private NodeIndexPair walkUpUntilNonLayoutOnly(
|
||||
private NodeIndexPair walkUpUntilNativeKindIsParent(
|
||||
ReactShadowNode node,
|
||||
int indexInNativeChildren) {
|
||||
while (node.isLayoutOnly()) {
|
||||
while (node.getNativeKind() != NativeKind.PARENT) {
|
||||
ReactShadowNode parent = node.getParent();
|
||||
if (parent == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
indexInNativeChildren = indexInNativeChildren + parent.getNativeOffsetForChild(node);
|
||||
indexInNativeChildren = indexInNativeChildren +
|
||||
(node.getNativeKind() == NativeKind.LEAF ? 1 : 0) +
|
||||
parent.getNativeOffsetForChild(node);
|
||||
node = parent;
|
||||
}
|
||||
|
||||
@@ -247,8 +269,8 @@ public class NativeViewHierarchyOptimizer {
|
||||
|
||||
private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int index) {
|
||||
int indexInNativeChildren = parent.getNativeOffsetForChild(parent.getChildAt(index));
|
||||
if (parent.isLayoutOnly()) {
|
||||
NodeIndexPair result = walkUpUntilNonLayoutOnly(parent, indexInNativeChildren);
|
||||
if (parent.getNativeKind() != NativeKind.PARENT) {
|
||||
NodeIndexPair result = walkUpUntilNativeKindIsParent(parent, indexInNativeChildren);
|
||||
if (result == null) {
|
||||
// If the parent hasn't been attached to its native parent yet, don't issue commands to the
|
||||
// native hierarchy. We'll do that when the parent node actually gets attached somewhere.
|
||||
@@ -258,20 +280,26 @@ public class NativeViewHierarchyOptimizer {
|
||||
indexInNativeChildren = result.index;
|
||||
}
|
||||
|
||||
if (!child.isLayoutOnly()) {
|
||||
addNonLayoutNode(parent, child, indexInNativeChildren);
|
||||
if (child.getNativeKind() != NativeKind.NONE) {
|
||||
addNativeChild(parent, child, indexInNativeChildren);
|
||||
} else {
|
||||
addLayoutOnlyNode(parent, child, indexInNativeChildren);
|
||||
addNonNativeChild(parent, child, indexInNativeChildren);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For handling node removal from manageChildren. In the case of removing a layout-only node, we
|
||||
* need to instead recursively remove all its children from their native parents.
|
||||
* For handling node removal from manageChildren. In the case of removing a node which isn't
|
||||
* hosting its own children (e.g. layout-only or NativeKind.LEAF), we need to recursively remove
|
||||
* all its children from their native parents.
|
||||
*/
|
||||
private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDelete) {
|
||||
ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent();
|
||||
if (nodeToRemove.getNativeKind() != NativeKind.PARENT) {
|
||||
for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
|
||||
removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete);
|
||||
}
|
||||
}
|
||||
|
||||
ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent();
|
||||
if (nativeNodeToRemoveFrom != null) {
|
||||
int index = nativeNodeToRemoveFrom.indexOfNativeChild(nodeToRemove);
|
||||
nativeNodeToRemoveFrom.removeNativeChildAt(index);
|
||||
@@ -282,21 +310,17 @@ public class NativeViewHierarchyOptimizer {
|
||||
null,
|
||||
shouldDelete ? new int[] {nodeToRemove.getReactTag()} : null,
|
||||
shouldDelete ? new int[] {index} : null);
|
||||
} else {
|
||||
for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
|
||||
removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addLayoutOnlyNode(
|
||||
ReactShadowNode nonLayoutOnlyNode,
|
||||
ReactShadowNode layoutOnlyNode,
|
||||
private void addNonNativeChild(
|
||||
ReactShadowNode nativeParent,
|
||||
ReactShadowNode nonNativeChild,
|
||||
int index) {
|
||||
addGrandchildren(nonLayoutOnlyNode, layoutOnlyNode, index);
|
||||
addGrandchildren(nativeParent, nonNativeChild, index);
|
||||
}
|
||||
|
||||
private void addNonLayoutNode(
|
||||
private void addNativeChild(
|
||||
ReactShadowNode parent,
|
||||
ReactShadowNode child,
|
||||
int index) {
|
||||
@@ -307,13 +331,17 @@ public class NativeViewHierarchyOptimizer {
|
||||
new ViewAtIndex[] {new ViewAtIndex(child.getReactTag(), index)},
|
||||
null,
|
||||
null);
|
||||
|
||||
if (child.getNativeKind() != NativeKind.PARENT) {
|
||||
addGrandchildren(parent, child, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void addGrandchildren(
|
||||
ReactShadowNode nativeParent,
|
||||
ReactShadowNode child,
|
||||
int index) {
|
||||
Assertions.assertCondition(!nativeParent.isLayoutOnly());
|
||||
Assertions.assertCondition(child.getNativeKind() != NativeKind.PARENT);
|
||||
|
||||
// `child` can't hold native children. Add all of `child`'s children to `parent`.
|
||||
int currentIndex = index;
|
||||
@@ -321,16 +349,15 @@ public class NativeViewHierarchyOptimizer {
|
||||
ReactShadowNode grandchild = child.getChildAt(i);
|
||||
Assertions.assertCondition(grandchild.getNativeParent() == null);
|
||||
|
||||
if (grandchild.isLayoutOnly()) {
|
||||
// Adding this child could result in adding multiple native views
|
||||
int grandchildCountBefore = nativeParent.getNativeChildCount();
|
||||
addLayoutOnlyNode(nativeParent, grandchild, currentIndex);
|
||||
int grandchildCountAfter = nativeParent.getNativeChildCount();
|
||||
currentIndex += grandchildCountAfter - grandchildCountBefore;
|
||||
// Adding this child could result in adding multiple native views
|
||||
int grandchildCountBefore = nativeParent.getNativeChildCount();
|
||||
if (grandchild.getNativeKind() == NativeKind.NONE) {
|
||||
addNonNativeChild(nativeParent, grandchild, currentIndex);
|
||||
} else {
|
||||
addNonLayoutNode(nativeParent, grandchild, currentIndex);
|
||||
currentIndex++;
|
||||
addNativeChild(nativeParent, grandchild, currentIndex);
|
||||
}
|
||||
int grandchildCountAfter = nativeParent.getNativeChildCount();
|
||||
currentIndex += grandchildCountAfter - grandchildCountBefore;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,10 +376,16 @@ public class NativeViewHierarchyOptimizer {
|
||||
int x = node.getScreenX();
|
||||
int y = node.getScreenY();
|
||||
|
||||
while (parent != null && parent.isLayoutOnly()) {
|
||||
// TODO(7854667): handle and test proper clipping
|
||||
x += Math.round(parent.getLayoutX());
|
||||
y += Math.round(parent.getLayoutY());
|
||||
while (parent != null && parent.getNativeKind() != NativeKind.PARENT) {
|
||||
if (!parent.isVirtual()) {
|
||||
// Skip these additions for virtual nodes. This has the same effect as `getLayout*`
|
||||
// returning `0`. Virtual nodes aren't in the Yoga tree so we can't call `getLayout*` on
|
||||
// them.
|
||||
|
||||
// TODO(7854667): handle and test proper clipping
|
||||
x += Math.round(parent.getLayoutX());
|
||||
y += Math.round(parent.getLayoutY());
|
||||
}
|
||||
|
||||
parent = parent.getParent();
|
||||
}
|
||||
@@ -361,10 +394,10 @@ public class NativeViewHierarchyOptimizer {
|
||||
}
|
||||
|
||||
private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) {
|
||||
if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) {
|
||||
if (toUpdate.getNativeKind() != NativeKind.NONE && toUpdate.getNativeParent() != null) {
|
||||
int tag = toUpdate.getReactTag();
|
||||
mUIViewOperationQueue.enqueueUpdateLayout(
|
||||
toUpdate.getNativeParent().getReactTag(),
|
||||
toUpdate.getLayoutParent().getReactTag(),
|
||||
tag,
|
||||
x,
|
||||
y,
|
||||
|
||||
@@ -48,15 +48,16 @@ public interface ReactShadowNode<T extends ReactShadowNode> {
|
||||
|
||||
/**
|
||||
* Nodes that return {@code true} will be treated as "virtual" nodes. That is, nodes that are not
|
||||
* mapped into native views (e.g. nested text node). By default this method returns {@code false}.
|
||||
* mapped into native views or Yoga nodes (e.g. nested text node). By default this method returns
|
||||
* {@code false}.
|
||||
*/
|
||||
boolean isVirtual();
|
||||
|
||||
/**
|
||||
* Nodes that return {@code true} will be treated as a root view for the virtual nodes tree. It
|
||||
* means that {@link NativeViewHierarchyManager} will not try to perform {@code manageChildren}
|
||||
* operation on such views. Good example is {@code InputText} view that may have children {@code
|
||||
* Text} nodes but this whole hierarchy will be mapped to a single android {@link EditText} view.
|
||||
* means that all of its descendants will be "virtual" nodes. Good example is {@code InputText}
|
||||
* view that may have children {@code Text} nodes but this whole hierarchy will be mapped to a
|
||||
* single android {@link EditText} view.
|
||||
*/
|
||||
boolean isVirtualAnchor();
|
||||
|
||||
@@ -68,6 +69,14 @@ public interface ReactShadowNode<T extends ReactShadowNode> {
|
||||
*/
|
||||
boolean isYogaLeafNode();
|
||||
|
||||
/**
|
||||
* When constructing the native tree, nodes that return {@code true} will be treated as leaves.
|
||||
* Instead of adding this view's native children as subviews of it, they will be added as subviews
|
||||
* of an ancestor. In other words, this view wants to support native children but it cannot host
|
||||
* them itself (e.g. it isn't a ViewGroup).
|
||||
*/
|
||||
boolean hoistNativeChildren();
|
||||
|
||||
String getViewClass();
|
||||
|
||||
boolean hasUpdates();
|
||||
@@ -99,7 +108,7 @@ public interface ReactShadowNode<T extends ReactShadowNode> {
|
||||
* layout. Will be only called for nodes that are marked as updated with {@link #markUpdated()} or
|
||||
* require layouting (marked with {@link #dirty()}).
|
||||
*/
|
||||
void onBeforeLayout();
|
||||
void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer);
|
||||
|
||||
void updateProperties(ReactStylesDiffMap props);
|
||||
|
||||
@@ -135,6 +144,12 @@ public interface ReactShadowNode<T extends ReactShadowNode> {
|
||||
@Nullable
|
||||
T getParent();
|
||||
|
||||
// Returns the node that is responsible for laying out this node.
|
||||
@Nullable
|
||||
T getLayoutParent();
|
||||
|
||||
void setLayoutParent(@Nullable T layoutParent);
|
||||
|
||||
/**
|
||||
* Get the {@link ThemedReactContext} associated with this {@link ReactShadowNode}. This will
|
||||
* never change during the lifetime of a {@link ReactShadowNode} instance, but different instances
|
||||
@@ -179,6 +194,8 @@ public interface ReactShadowNode<T extends ReactShadowNode> {
|
||||
|
||||
boolean isLayoutOnly();
|
||||
|
||||
NativeKind getNativeKind();
|
||||
|
||||
int getTotalNativeChildren();
|
||||
|
||||
boolean isDescendantOf(T ancestorNode);
|
||||
@@ -354,4 +371,6 @@ public interface ReactShadowNode<T extends ReactShadowNode> {
|
||||
Integer getWidthMeasureSpec();
|
||||
|
||||
Integer getHeightMeasureSpec();
|
||||
|
||||
Iterable<? extends ReactShadowNode> calculateLayoutOnChildren();
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
private boolean mNodeUpdated = true;
|
||||
private @Nullable ArrayList<ReactShadowNodeImpl> mChildren;
|
||||
private @Nullable ReactShadowNodeImpl mParent;
|
||||
private @Nullable ReactShadowNodeImpl mLayoutParent;
|
||||
|
||||
// layout-only nodes
|
||||
private boolean mIsLayoutOnly;
|
||||
@@ -98,7 +99,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
|
||||
/**
|
||||
* Nodes that return {@code true} will be treated as "virtual" nodes. That is, nodes that are not
|
||||
* mapped into native views (e.g. nested text node). By default this method returns {@code false}.
|
||||
* mapped into native views or Yoga nodes (e.g. nested text node). By default this method returns
|
||||
* {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean isVirtual() {
|
||||
@@ -107,9 +109,9 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
|
||||
/**
|
||||
* Nodes that return {@code true} will be treated as a root view for the virtual nodes tree. It
|
||||
* means that {@link NativeViewHierarchyManager} will not try to perform {@code manageChildren}
|
||||
* operation on such views. Good example is {@code InputText} view that may have children {@code
|
||||
* Text} nodes but this whole hierarchy will be mapped to a single android {@link EditText} view.
|
||||
* means that all of its descendants will be "virtual" nodes. Good example is {@code InputText}
|
||||
* view that may have children {@code Text} nodes but this whole hierarchy will be mapped to a
|
||||
* single android {@link EditText} view.
|
||||
*/
|
||||
@Override
|
||||
public boolean isVirtualAnchor() {
|
||||
@@ -127,6 +129,17 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return isMeasureDefined();
|
||||
}
|
||||
|
||||
/**
|
||||
* When constructing the native tree, nodes that return {@code true} will be treated as leaves.
|
||||
* Instead of adding this view's native children as subviews of it, they will be added as subviews
|
||||
* of an ancestor. In other words, this view wants to support native children but it cannot host
|
||||
* them itself (e.g. it isn't a ViewGroup).
|
||||
*/
|
||||
@Override
|
||||
public boolean hoistNativeChildren() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getViewClass() {
|
||||
return Assertions.assertNotNull(mViewClassName);
|
||||
@@ -166,6 +179,18 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
public void dirty() {
|
||||
if (!isVirtual()) {
|
||||
mYogaNode.dirty();
|
||||
} else if (getParent() != null) {
|
||||
// Virtual nodes aren't involved in layout but they need to have the dirty signal
|
||||
// propagated to their ancestors.
|
||||
//
|
||||
// TODO: There are some edge cases that currently aren't supported. For example, if the size
|
||||
// of your inline image/view changes, its size on-screen is not be updated. Similarly,
|
||||
// if the size of a view inside of an inline view changes, its size on-screen is not
|
||||
// updated. The problem may be that dirty propagation stops at inline views because the
|
||||
// parent of each inline view is null. A possible fix would be to implement an `onDirty`
|
||||
// handler in Yoga that will propagate the dirty signal to the ancestors of the inline view.
|
||||
//
|
||||
getParent().dirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +224,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
markUpdated();
|
||||
|
||||
int increase = child.isLayoutOnly() ? child.getTotalNativeChildren() : 1;
|
||||
int increase = child.getTotalNativeNodeContributionToParent();
|
||||
mTotalNativeChildren += increase;
|
||||
|
||||
updateNativeChildrenCountInParent(increase);
|
||||
@@ -219,7 +244,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
markUpdated();
|
||||
|
||||
int decrease = removed.isLayoutOnly() ? removed.getTotalNativeChildren() : 1;
|
||||
int decrease = removed.getTotalNativeNodeContributionToParent();
|
||||
mTotalNativeChildren -= decrease;
|
||||
updateNativeChildrenCountInParent(-decrease);
|
||||
return removed;
|
||||
@@ -257,9 +282,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
ReactShadowNodeImpl toRemove = getChildAt(i);
|
||||
toRemove.mParent = null;
|
||||
decrease += toRemove.getTotalNativeNodeContributionToParent();
|
||||
toRemove.dispose();
|
||||
|
||||
decrease += toRemove.isLayoutOnly() ? toRemove.getTotalNativeChildren() : 1;
|
||||
}
|
||||
Assertions.assertNotNull(mChildren).clear();
|
||||
markUpdated();
|
||||
@@ -269,11 +293,11 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
|
||||
private void updateNativeChildrenCountInParent(int delta) {
|
||||
if (mIsLayoutOnly) {
|
||||
if (getNativeKind() != NativeKind.PARENT) {
|
||||
ReactShadowNodeImpl parent = getParent();
|
||||
while (parent != null) {
|
||||
parent.mTotalNativeChildren += delta;
|
||||
if (!parent.isLayoutOnly()) {
|
||||
if (parent.getNativeKind() == NativeKind.PARENT) {
|
||||
break;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
@@ -287,7 +311,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
* require layouting (marked with {@link #dirty()}).
|
||||
*/
|
||||
@Override
|
||||
public void onBeforeLayout() {}
|
||||
public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void updateProperties(ReactStylesDiffMap props) {
|
||||
@@ -397,6 +422,17 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return mParent;
|
||||
}
|
||||
|
||||
// Returns the node that is responsible for laying out this node.
|
||||
@Override
|
||||
public final @Nullable ReactShadowNodeImpl getLayoutParent() {
|
||||
return mLayoutParent != null ? mLayoutParent : getNativeParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setLayoutParent(@Nullable ReactShadowNodeImpl layoutParent) {
|
||||
mLayoutParent = layoutParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ThemedReactContext} associated with this {@link ReactShadowNodeImpl}. This will
|
||||
* never change during the lifetime of a {@link ReactShadowNodeImpl} instance, but different
|
||||
@@ -446,8 +482,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
*/
|
||||
@Override
|
||||
public final void addNativeChildAt(ReactShadowNodeImpl child, int nativeIndex) {
|
||||
Assertions.assertCondition(!mIsLayoutOnly);
|
||||
Assertions.assertCondition(!child.mIsLayoutOnly);
|
||||
Assertions.assertCondition(getNativeKind() == NativeKind.PARENT);
|
||||
Assertions.assertCondition(child.getNativeKind() != NativeKind.NONE);
|
||||
|
||||
if (mNativeChildren == null) {
|
||||
mNativeChildren = new ArrayList<>(4);
|
||||
@@ -508,6 +544,14 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return mIsLayoutOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeKind getNativeKind() {
|
||||
return
|
||||
isVirtual() || isLayoutOnly() ? NativeKind.NONE :
|
||||
hoistNativeChildren() ? NativeKind.LEAF :
|
||||
NativeKind.PARENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getTotalNativeChildren() {
|
||||
return mTotalNativeChildren;
|
||||
@@ -531,6 +575,14 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return isDescendant;
|
||||
}
|
||||
|
||||
private int getTotalNativeNodeContributionToParent() {
|
||||
NativeKind kind = getNativeKind();
|
||||
return
|
||||
kind == NativeKind.NONE ? mTotalNativeChildren :
|
||||
kind == NativeKind.LEAF ? 1 + mTotalNativeChildren :
|
||||
1; // kind == NativeKind.PARENT
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + mViewClassName + " " + getReactTag() + "]";
|
||||
@@ -585,7 +637,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
index += (current.isLayoutOnly() ? current.getTotalNativeChildren() : 1);
|
||||
index += current.getTotalNativeNodeContributionToParent();
|
||||
}
|
||||
if (!found) {
|
||||
throw new RuntimeException(
|
||||
@@ -978,4 +1030,13 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
public Integer getHeightMeasureSpec() {
|
||||
return mHeightMeasureSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends ReactShadowNode> calculateLayoutOnChildren() {
|
||||
return isVirtualAnchor() ?
|
||||
// All of the descendants are virtual so none of them are involved in layout.
|
||||
null :
|
||||
// Just return the children. Flexbox calculations have already been run on them.
|
||||
mChildren;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,15 +429,13 @@ public class UIImplementation {
|
||||
cssNodeToManage.addChildAt(cssNodeToAdd, viewAtIndex.mIndex);
|
||||
}
|
||||
|
||||
if (!cssNodeToManage.isVirtual() && !cssNodeToManage.isVirtualAnchor()) {
|
||||
mNativeViewHierarchyOptimizer.handleManageChildren(
|
||||
cssNodeToManage,
|
||||
indicesToRemove,
|
||||
tagsToRemove,
|
||||
viewsToAdd,
|
||||
tagsToDelete,
|
||||
indicesToDelete);
|
||||
}
|
||||
mNativeViewHierarchyOptimizer.handleManageChildren(
|
||||
cssNodeToManage,
|
||||
indicesToRemove,
|
||||
tagsToRemove,
|
||||
viewsToAdd,
|
||||
tagsToDelete,
|
||||
indicesToDelete);
|
||||
|
||||
for (int i = 0; i < tagsToDelete.length; i++) {
|
||||
removeShadowNode(mShadowNodeRegistry.getNode(tagsToDelete[i]));
|
||||
@@ -467,11 +465,9 @@ public class UIImplementation {
|
||||
cssNodeToManage.addChildAt(cssNodeToAdd, i);
|
||||
}
|
||||
|
||||
if (!cssNodeToManage.isVirtual() && !cssNodeToManage.isVirtualAnchor()) {
|
||||
mNativeViewHierarchyOptimizer.handleSetChildren(
|
||||
cssNodeToManage,
|
||||
childrenTags);
|
||||
}
|
||||
mNativeViewHierarchyOptimizer.handleSetChildren(
|
||||
cssNodeToManage,
|
||||
childrenTags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,7 +760,7 @@ public class UIImplementation {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node.isVirtual() || node.isLayoutOnly()) {
|
||||
while (node.getNativeKind() == NativeKind.NONE) {
|
||||
node = node.getParent();
|
||||
}
|
||||
mOperationsQueue.enqueueSetJSResponder(node.getReactTag(), reactTag, blockNativeResponder);
|
||||
@@ -903,14 +899,14 @@ public class UIImplementation {
|
||||
|
||||
private void assertNodeDoesNotNeedCustomLayoutForChildren(ReactShadowNode node) {
|
||||
ViewManager viewManager = Assertions.assertNotNull(mViewManagers.get(node.getViewClass()));
|
||||
ViewGroupManager viewGroupManager;
|
||||
if (viewManager instanceof ViewGroupManager) {
|
||||
viewGroupManager = (ViewGroupManager) viewManager;
|
||||
IViewManagerWithChildren viewManagerWithChildren;
|
||||
if (viewManager instanceof IViewManagerWithChildren) {
|
||||
viewManagerWithChildren = (IViewManagerWithChildren) viewManager;
|
||||
} else {
|
||||
throw new IllegalViewOperationException("Trying to use view " + node.getViewClass() +
|
||||
" as a parent, but its Manager doesn't extends ViewGroupManager");
|
||||
}
|
||||
if (viewGroupManager != null && viewGroupManager.needsCustomLayoutForChildren()) {
|
||||
if (viewManagerWithChildren != null && viewManagerWithChildren.needsCustomLayoutForChildren()) {
|
||||
throw new IllegalViewOperationException(
|
||||
"Trying to measure a view using measureLayout/measureLayoutRelativeToParent relative to" +
|
||||
" an ancestor that requires custom layout for it's children (" + node.getViewClass() +
|
||||
@@ -925,7 +921,7 @@ public class UIImplementation {
|
||||
for (int i = 0; i < cssNode.getChildCount(); i++) {
|
||||
notifyOnBeforeLayoutRecursive(cssNode.getChildAt(i));
|
||||
}
|
||||
cssNode.onBeforeLayout();
|
||||
cssNode.onBeforeLayout(mNativeViewHierarchyOptimizer);
|
||||
}
|
||||
|
||||
protected void calculateRootLayout(ReactShadowNode cssRoot) {
|
||||
@@ -957,10 +953,11 @@ public class UIImplementation {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cssNode.isVirtualAnchor()) {
|
||||
for (int i = 0; i < cssNode.getChildCount(); i++) {
|
||||
Iterable<? extends ReactShadowNode> cssChildren = cssNode.calculateLayoutOnChildren();
|
||||
if (cssChildren != null) {
|
||||
for (ReactShadowNode cssChild : cssChildren) {
|
||||
applyUpdatesRecursive(
|
||||
cssNode.getChildAt(i),
|
||||
cssChild,
|
||||
absoluteX + cssNode.getLayoutX(),
|
||||
absoluteY + cssNode.getLayoutY());
|
||||
}
|
||||
|
||||
@@ -878,4 +878,9 @@ public class UIManagerModule extends ReactContextBaseJavaModule
|
||||
@Override
|
||||
public void onLowMemory() {}
|
||||
}
|
||||
|
||||
public View resolveView(int tag) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
return mUIImplementation.getUIViewOperationQueue().getNativeViewHierarchyManager().resolveView(tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import javax.annotation.Nullable;
|
||||
* Class providing children management API for view managers of classes extending ViewGroup.
|
||||
*/
|
||||
public abstract class ViewGroupManager <T extends ViewGroup>
|
||||
extends BaseViewManager<T, LayoutShadowNode> {
|
||||
extends BaseViewManager<T, LayoutShadowNode>
|
||||
implements IViewManagerWithChildren {
|
||||
|
||||
private static WeakHashMap<View, Integer> mZIndexHash = new WeakHashMap<>();
|
||||
|
||||
@@ -97,6 +98,7 @@ public abstract class ViewGroupManager <T extends ViewGroup>
|
||||
* through the ViewGroup's onLayout method. In that case, onLayout for this View type must *not*
|
||||
* call layout on its children.
|
||||
*/
|
||||
@Override
|
||||
public boolean needsCustomLayoutForChildren() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -14,18 +14,26 @@ import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.view.Gravity;
|
||||
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||
import com.facebook.react.uimanager.LayoutShadowNode;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactShadowNode;
|
||||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.yoga.YogaConstants;
|
||||
import com.facebook.yoga.YogaDirection;
|
||||
import com.facebook.yoga.YogaUnit;
|
||||
import com.facebook.yoga.YogaValue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
@@ -41,7 +49,10 @@ import javax.annotation.Nullable;
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
|
||||
private static final String INLINE_IMAGE_PLACEHOLDER = "I";
|
||||
// Use a direction weak character so the placeholder doesn't change the direction of the previous
|
||||
// character.
|
||||
// https://en.wikipedia.org/wiki/Bi-directional_text#weak_characters
|
||||
private static final String INLINE_VIEW_PLACEHOLDER = "0";
|
||||
public static final int UNSET = -1;
|
||||
|
||||
public static final String PROP_SHADOW_OFFSET = "textShadowOffset";
|
||||
@@ -84,6 +95,8 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
SpannableStringBuilder sb,
|
||||
List<SetSpanOperation> ops,
|
||||
TextAttributes parentTextAttributes,
|
||||
boolean supportsInlineViews,
|
||||
Map<Integer, ReactShadowNode> inlineViews,
|
||||
int start) {
|
||||
|
||||
TextAttributes textAttributes;
|
||||
@@ -102,19 +115,39 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
((ReactRawTextShadowNode) child).getText(),
|
||||
textAttributes.getTextTransform()));
|
||||
} else if (child instanceof ReactBaseTextShadowNode) {
|
||||
buildSpannedFromShadowNode((ReactBaseTextShadowNode) child, sb, ops, textAttributes, sb.length());
|
||||
buildSpannedFromShadowNode((ReactBaseTextShadowNode) child, sb, ops, textAttributes, supportsInlineViews, inlineViews, sb.length());
|
||||
} else if (child instanceof ReactTextInlineImageShadowNode) {
|
||||
// We make the image take up 1 character in the span and put a corresponding character into
|
||||
// the text so that the image doesn't run over any following text.
|
||||
sb.append(INLINE_IMAGE_PLACEHOLDER);
|
||||
sb.append(INLINE_VIEW_PLACEHOLDER);
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
sb.length() - INLINE_IMAGE_PLACEHOLDER.length(),
|
||||
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
|
||||
sb.length(),
|
||||
((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
|
||||
} else if (supportsInlineViews) {
|
||||
int reactTag = child.getReactTag();
|
||||
YogaValue widthValue = child.getStyleWidth();
|
||||
YogaValue heightValue = child.getStyleHeight();
|
||||
|
||||
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
|
||||
throw new IllegalViewOperationException("Views nested within a <Text> must have a width and height");
|
||||
}
|
||||
float width = widthValue.value;
|
||||
float height = heightValue.value;
|
||||
|
||||
// We make the inline view take up 1 character in the span and put a corresponding character into
|
||||
// the text so that the inline view doesn't run over any following text.
|
||||
sb.append(INLINE_VIEW_PLACEHOLDER);
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
|
||||
sb.length(),
|
||||
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
|
||||
inlineViews.put(reactTag, child);
|
||||
} else {
|
||||
throw new IllegalViewOperationException(
|
||||
"Unexpected view type nested under text node: " + child.getClass());
|
||||
"Unexpected view type nested under a <Text> or <TextInput> node: " + child.getClass());
|
||||
}
|
||||
child.markUpdateSeen();
|
||||
}
|
||||
@@ -192,8 +225,15 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
}
|
||||
}
|
||||
|
||||
// `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`.
|
||||
protected static Spannable spannedFromShadowNode(
|
||||
ReactBaseTextShadowNode textShadowNode, String text) {
|
||||
ReactBaseTextShadowNode textShadowNode,
|
||||
String text,
|
||||
boolean supportsInlineViews,
|
||||
NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
|
||||
Assertions.assertCondition(
|
||||
!supportsInlineViews || nativeViewHierarchyOptimizer != null,
|
||||
"nativeViewHierarchyOptimizer is required when inline views are supported");
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||
|
||||
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
|
||||
@@ -202,6 +242,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
// up-to-bottom, otherwise all the spannables that are withing the region for which one may set
|
||||
// a new spannable will be wiped out
|
||||
List<SetSpanOperation> ops = new ArrayList<>();
|
||||
Map<Integer, ReactShadowNode> inlineViews = supportsInlineViews ? new HashMap<Integer, ReactShadowNode>() : null;
|
||||
|
||||
if (text != null) {
|
||||
// Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on
|
||||
@@ -209,20 +250,37 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
sb.append(TextTransform.apply(text, textShadowNode.mTextAttributes.getTextTransform()));
|
||||
}
|
||||
|
||||
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, 0);
|
||||
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, supportsInlineViews, inlineViews, 0);
|
||||
|
||||
textShadowNode.mContainsImages = false;
|
||||
float heightOfTallestInlineImage = Float.NaN;
|
||||
textShadowNode.mInlineViews = inlineViews;
|
||||
float heightOfTallestInlineViewOrImage = Float.NaN;
|
||||
|
||||
// While setting the Spans on the final text, we also check whether any of them are images.
|
||||
// While setting the Spans on the final text, we also check whether any of them are inline views
|
||||
// or images.
|
||||
int priority = 0;
|
||||
for (SetSpanOperation op : ops) {
|
||||
if (op.what instanceof TextInlineImageSpan) {
|
||||
int height = ((TextInlineImageSpan) op.what).getHeight();
|
||||
textShadowNode.mContainsImages = true;
|
||||
if (Float.isNaN(heightOfTallestInlineImage)
|
||||
|| height > heightOfTallestInlineImage) {
|
||||
heightOfTallestInlineImage = height;
|
||||
boolean isInlineImage = op.what instanceof TextInlineImageSpan;
|
||||
if (isInlineImage || op.what instanceof TextInlineViewPlaceholderSpan) {
|
||||
int height;
|
||||
if (isInlineImage) {
|
||||
height = ((TextInlineImageSpan)op.what).getHeight();
|
||||
textShadowNode.mContainsImages = true;
|
||||
} else {
|
||||
TextInlineViewPlaceholderSpan placeholder = (TextInlineViewPlaceholderSpan) op.what;
|
||||
height = placeholder.getHeight();
|
||||
|
||||
// Inline views cannot be layout-only because the ReactTextView needs to be able to grab
|
||||
// ahold of them on the UI thread to size and position them.
|
||||
ReactShadowNode childNode = inlineViews.get(placeholder.getReactTag());
|
||||
nativeViewHierarchyOptimizer.handleForceViewToBeNonLayoutOnly(childNode);
|
||||
|
||||
// The ReactTextView is responsible for laying out the inline views.
|
||||
childNode.setLayoutParent(textShadowNode);
|
||||
}
|
||||
|
||||
if (Float.isNaN(heightOfTallestInlineViewOrImage) || height > heightOfTallestInlineViewOrImage) {
|
||||
heightOfTallestInlineViewOrImage = height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +290,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
priority++;
|
||||
}
|
||||
|
||||
textShadowNode.mTextAttributes.setHeightOfTallestInlineImage(heightOfTallestInlineImage);
|
||||
textShadowNode.mTextAttributes.setHeightOfTallestInlineViewOrImage(heightOfTallestInlineViewOrImage);
|
||||
|
||||
return sb;
|
||||
}
|
||||
@@ -305,7 +363,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
protected @Nullable String mFontFamily = null;
|
||||
|
||||
protected boolean mContainsImages = false;
|
||||
protected float mHeightOfTallestInlineImage = Float.NaN;
|
||||
protected Map<Integer, ReactShadowNode> mInlineViews;
|
||||
|
||||
public ReactBaseTextShadowNode() {
|
||||
mTextAttributes = new TextAttributes();
|
||||
@@ -403,8 +461,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
|
||||
@ReactProp(name = ViewProps.BACKGROUND_COLOR)
|
||||
public void setBackgroundColor(Integer color) {
|
||||
// Don't apply background color to anchor TextView since it will be applied on the View directly
|
||||
if (!isVirtualAnchor()) {
|
||||
// Background color needs to be handled here for virtual nodes so it can be incorporated into
|
||||
// the span. However, it doesn't need to be applied to non-virtual nodes because non-virtual
|
||||
// nodes get mapped to native views and native views get their background colors get set via
|
||||
// {@link BaseViewManager}.
|
||||
if (isVirtual()) {
|
||||
mIsBackgroundColorSet = (color != null);
|
||||
if (mIsBackgroundColorSet) {
|
||||
mBackgroundColor = color;
|
||||
|
||||
@@ -183,4 +183,9 @@ public abstract class ReactTextAnchorViewManager<T extends View, C extends React
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "onInlineViewLayout")
|
||||
public void setNotifyOnInlineViewLayout(ReactTextView view, boolean notifyOnInlineViewLayout) {
|
||||
view.setNotifyOnInlineViewLayout(notifyOnInlineViewLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
|
||||
import com.facebook.react.uimanager.ReactShadowNode;
|
||||
import com.facebook.react.uimanager.Spacing;
|
||||
import com.facebook.react.uimanager.UIViewOperationQueue;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
@@ -30,6 +32,9 @@ import com.facebook.yoga.YogaMeasureFunction;
|
||||
import com.facebook.yoga.YogaMeasureMode;
|
||||
import com.facebook.yoga.YogaMeasureOutput;
|
||||
import com.facebook.yoga.YogaNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
@@ -189,13 +194,25 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeLayout() {
|
||||
mPreparedSpannableText = spannedFromShadowNode(this, null);
|
||||
public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
|
||||
mPreparedSpannableText = spannedFromShadowNode(
|
||||
this,
|
||||
/* text (e.g. from `value` prop): */ null,
|
||||
/* supportsInlineViews: */ true,
|
||||
nativeViewHierarchyOptimizer);
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVirtualAnchor() {
|
||||
// Text's descendants aren't necessarily all virtual nodes. Text can contain a combination of
|
||||
// virtual and non-virtual (e.g. inline views) nodes. Therefore it's not a virtual anchor
|
||||
// by the doc comment on {@link ReactShadowNode#isVirtualAnchor}.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hoistNativeChildren() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -231,4 +248,27 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
|
||||
public void setShouldNotifyOnTextLayout(boolean shouldNotifyOnTextLayout) {
|
||||
mShouldNotifyOnTextLayout = shouldNotifyOnTextLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends ReactShadowNode> calculateLayoutOnChildren() {
|
||||
// Run flexbox on and return the descendants which are inline views.
|
||||
|
||||
if (mInlineViews == null || mInlineViews.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Spanned text = Assertions.assertNotNull(
|
||||
this.mPreparedSpannableText,
|
||||
"Spannable element has not been prepared in onBeforeLayout");
|
||||
TextInlineViewPlaceholderSpan[] placeholders = text.getSpans(0, text.length(), TextInlineViewPlaceholderSpan.class);
|
||||
ArrayList<ReactShadowNode> shadowNodes = new ArrayList<ReactShadowNode>(placeholders.length);
|
||||
|
||||
for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
|
||||
ReactShadowNode child = mInlineViews.get(placeholder.getReactTag());
|
||||
child.calculateLayout();
|
||||
shadowNodes.add(child);
|
||||
}
|
||||
|
||||
return shadowNodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,35 @@ import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.appcompat.widget.TintContextWrapper;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactCompoundView;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.ViewDefaults;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class ReactTextView extends AppCompatTextView implements ReactCompoundView {
|
||||
|
||||
private static final ViewGroup.LayoutParams EMPTY_LAYOUT_PARAMS =
|
||||
@@ -39,6 +52,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
|
||||
private int mNumberOfLines = ViewDefaults.NUMBER_OF_LINES;
|
||||
private TextUtils.TruncateAt mEllipsizeLocation = TextUtils.TruncateAt.END;
|
||||
private int mLinkifyMaskType = 0;
|
||||
private boolean mNotifyOnInlineViewLayout;
|
||||
|
||||
private ReactViewBackgroundManager mReactBackgroundManager;
|
||||
private Spannable mSpanned;
|
||||
@@ -51,6 +65,185 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
|
||||
mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
|
||||
}
|
||||
|
||||
private WritableMap inlineViewJson(int visibility, int index, int left, int top, int right, int bottom) {
|
||||
WritableMap json = Arguments.createMap();
|
||||
if (visibility == View.GONE) {
|
||||
json.putString("visibility", "gone");
|
||||
json.putInt("index", index);
|
||||
} else if (visibility == View.VISIBLE) {
|
||||
json.putString("visibility", "visible");
|
||||
json.putInt("index", index);
|
||||
json.putDouble("left", PixelUtil.toDIPFromPixel(left));
|
||||
json.putDouble("top", PixelUtil.toDIPFromPixel(top));
|
||||
json.putDouble("right", PixelUtil.toDIPFromPixel(right));
|
||||
json.putDouble("bottom", PixelUtil.toDIPFromPixel(bottom));
|
||||
} else {
|
||||
json.putString("visibility", "unknown");
|
||||
json.putInt("index", index);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private ReactContext getReactContext() {
|
||||
Context context = getContext();
|
||||
return (context instanceof TintContextWrapper)
|
||||
? (ReactContext)((TintContextWrapper)context).getBaseContext()
|
||||
: (ReactContext)context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed,
|
||||
int textViewLeft,
|
||||
int textViewTop,
|
||||
int textViewRight,
|
||||
int textViewBottom) {
|
||||
if (!(getText() instanceof Spanned)) {
|
||||
/**
|
||||
* In general, {@link #setText} is called via {@link ReactTextViewManager#updateExtraData}
|
||||
* before we are laid out. This ordering is a requirement because we utilize the data from
|
||||
* setText in onLayout.
|
||||
*
|
||||
* However, it's possible for us to get an extra layout before we've received our setText
|
||||
* call. If this happens before the initial setText call, then getText() will have its default
|
||||
* value which isn't a Spanned and we need to bail out. That's fine because we'll get a
|
||||
* setText followed by a layout later.
|
||||
*
|
||||
* The cause for the extra early layout is that an ancestor gets transitioned from a
|
||||
* layout-only node to a non layout-only node.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
UIManagerModule uiManager = getReactContext().getNativeModule(UIManagerModule.class);
|
||||
|
||||
Spanned text = (Spanned) getText();
|
||||
Layout layout = getLayout();
|
||||
TextInlineViewPlaceholderSpan[] placeholders = text.getSpans(0, text.length(), TextInlineViewPlaceholderSpan.class);
|
||||
ArrayList inlineViewInfoArray = mNotifyOnInlineViewLayout ? new ArrayList(placeholders.length) : null;
|
||||
int textViewWidth = textViewRight - textViewLeft;
|
||||
int textViewHeight = textViewBottom - textViewTop;
|
||||
|
||||
for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
|
||||
View child = uiManager.resolveView(placeholder.getReactTag());
|
||||
|
||||
int start = text.getSpanStart(placeholder);
|
||||
int line = layout.getLineForOffset(start);
|
||||
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
|
||||
|
||||
if (// This truncation check works well on recent versions of Android (tested on 5.1.1 and
|
||||
// 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on
|
||||
// Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the first
|
||||
// thing to be truncated.
|
||||
(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line)) ||
|
||||
|
||||
// This truncation check works well on Android 4.4.4 but not on others (e.g. 6.0.1).
|
||||
// On Android 4.4.4, getLineEnd returns the first truncated character whereas on 6.0.1,
|
||||
// it appears to return the position after the last character on the line even if that
|
||||
// character is truncated.
|
||||
line >= mNumberOfLines || start >= layout.getLineEnd(line)) {
|
||||
// On some versions of Android (e.g. 4.4.4, 5.1.1), getPrimaryHorizontal can infinite
|
||||
// loop when called on a character that appears after the ellipsis. Avoid this bug by
|
||||
// special casing the character truncation case.
|
||||
child.setVisibility(View.GONE);
|
||||
if (mNotifyOnInlineViewLayout) {
|
||||
inlineViewInfoArray.add(inlineViewJson(View.GONE, start, -1, -1, -1, -1));
|
||||
}
|
||||
} else {
|
||||
int width = placeholder.getWidth();
|
||||
int height = placeholder.getHeight();
|
||||
|
||||
// Calculate if the direction of the placeholder character is Right-To-Left.
|
||||
boolean isRtlChar = layout.isRtlCharAt(start);
|
||||
|
||||
boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
|
||||
|
||||
int placeholderHorizontalPosition;
|
||||
// There's a bug on Samsung devices where calling getPrimaryHorizontal on
|
||||
// the last offset in the layout will result in an endless loop. Work around
|
||||
// this bug by avoiding getPrimaryHorizontal in that case.
|
||||
if (start == text.length() - 1) {
|
||||
placeholderHorizontalPosition = isRtlParagraph
|
||||
// Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns incorrect
|
||||
// values when the paragraph is RTL and `setSingleLine(true)`.
|
||||
? textViewWidth - (int)layout.getLineWidth(line)
|
||||
: (int) layout.getLineRight(line) - width;
|
||||
} else {
|
||||
// The direction of the paragraph may not be exactly the direction the string is heading in at the
|
||||
// position of the placeholder. So, if the direction of the character is the same as the paragraph
|
||||
// use primary, secondary otherwise.
|
||||
boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar;
|
||||
|
||||
placeholderHorizontalPosition = characterAndParagraphDirectionMatch
|
||||
? (int) layout.getPrimaryHorizontal(start)
|
||||
: (int) layout.getSecondaryHorizontal(start);
|
||||
|
||||
if (isRtlParagraph) {
|
||||
// Adjust `placeholderHorizontalPosition` to work around an Android bug.
|
||||
// The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout
|
||||
// methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and
|
||||
// `getLineRight` return incorrect values. Their return values seem to be off
|
||||
// by the same number of pixels so subtracting these values cancels out the error.
|
||||
//
|
||||
// The result is equivalent to bugless versions of `getPrimaryHorizontal`/`getSecondaryHorizontal`.
|
||||
placeholderHorizontalPosition = textViewWidth - ((int)layout.getLineRight(line) - placeholderHorizontalPosition);
|
||||
}
|
||||
|
||||
if (isRtlChar) {
|
||||
placeholderHorizontalPosition -= width;
|
||||
}
|
||||
}
|
||||
|
||||
int leftRelativeToTextView = isRtlChar
|
||||
? placeholderHorizontalPosition + getTotalPaddingRight()
|
||||
: placeholderHorizontalPosition + getTotalPaddingLeft();
|
||||
|
||||
int left = textViewLeft + leftRelativeToTextView;
|
||||
|
||||
// Vertically align the inline view to the baseline of the line of text.
|
||||
int topRelativeToTextView = getTotalPaddingTop() + layout.getLineBaseline(line) - height;
|
||||
int top = textViewTop + topRelativeToTextView;
|
||||
|
||||
boolean isFullyClipped = textViewWidth <= leftRelativeToTextView || textViewHeight <= topRelativeToTextView;
|
||||
int layoutVisibility = isFullyClipped ? View.GONE : View.VISIBLE;
|
||||
int layoutLeft = left;
|
||||
int layoutTop = top;
|
||||
int layoutRight = left + width;
|
||||
int layoutBottom = top + height;
|
||||
|
||||
// Keep these parameters in sync with what goes into `inlineViewInfoArray`.
|
||||
child.setVisibility(layoutVisibility);
|
||||
child.layout(layoutLeft, layoutTop, layoutRight, layoutBottom);
|
||||
if (mNotifyOnInlineViewLayout) {
|
||||
inlineViewInfoArray.add(
|
||||
inlineViewJson(layoutVisibility, start, layoutLeft, layoutTop, layoutRight, layoutBottom));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mNotifyOnInlineViewLayout) {
|
||||
Collections.sort(inlineViewInfoArray, new Comparator() {
|
||||
@Override
|
||||
public int compare(Object o1, Object o2) {
|
||||
WritableMap m1 = (WritableMap)o1;
|
||||
WritableMap m2 = (WritableMap)o2;
|
||||
return m1.getInt("index") - m2.getInt("index");
|
||||
}
|
||||
});
|
||||
WritableArray inlineViewInfoArray2 = Arguments.createArray();
|
||||
for (Object item : inlineViewInfoArray) {
|
||||
inlineViewInfoArray2.pushMap((WritableMap)item);
|
||||
}
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putArray("inlineViews", inlineViewInfoArray2);
|
||||
getReactContext().getJSModule(RCTEventEmitter.class).receiveEvent(
|
||||
getId(),
|
||||
"topInlineViewLayout",
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void setText(ReactTextUpdate update) {
|
||||
mContainsImages = update.containsImages();
|
||||
// Android's TextView crashes when it tries to relayout if LayoutParams are
|
||||
@@ -86,6 +279,9 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
|
||||
setJustificationMode(update.getJustificationMode());
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure onLayout is called so the inline views can be repositioned.
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -248,6 +444,10 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
|
||||
mEllipsizeLocation = ellipsizeLocation;
|
||||
}
|
||||
|
||||
public void setNotifyOnInlineViewLayout(boolean notifyOnInlineViewLayout) {
|
||||
mNotifyOnInlineViewLayout = notifyOnInlineViewLayout;
|
||||
}
|
||||
|
||||
public void updateView() {
|
||||
@Nullable TextUtils.TruncateAt ellipsizeLocation = mNumberOfLines == ViewDefaults.NUMBER_OF_LINES ? null : mEllipsizeLocation;
|
||||
setEllipsize(ellipsizeLocation);
|
||||
|
||||
@@ -13,10 +13,12 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.uimanager.IViewManagerWithChildren;
|
||||
import com.facebook.react.uimanager.ReactStylesDiffMap;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.yoga.YogaMeasureMode;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
@@ -25,7 +27,8 @@ import javax.annotation.Nullable;
|
||||
*/
|
||||
@ReactModule(name = ReactTextViewManager.REACT_CLASS)
|
||||
public class ReactTextViewManager
|
||||
extends ReactTextAnchorViewManager<ReactTextView, ReactTextShadowNode> {
|
||||
extends ReactTextAnchorViewManager<ReactTextView, ReactTextShadowNode>
|
||||
implements IViewManagerWithChildren {
|
||||
|
||||
@VisibleForTesting public static final String REACT_CLASS = "RCTText";
|
||||
|
||||
@@ -65,6 +68,10 @@ public class ReactTextViewManager
|
||||
view.updateView();
|
||||
}
|
||||
|
||||
public boolean needsCustomLayoutForChildren() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object updateLocalData(
|
||||
ReactTextView view, ReactStylesDiffMap props, ReactStylesDiffMap localData) {
|
||||
@@ -98,7 +105,9 @@ public class ReactTextViewManager
|
||||
|
||||
@Override
|
||||
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
|
||||
return MapBuilder.of("topTextLayout", MapBuilder.of("registrationName", "onTextLayout"));
|
||||
return MapBuilder.of(
|
||||
"topTextLayout", MapBuilder.of("registrationName", "onTextLayout"),
|
||||
"topInlineViewLayout", MapBuilder.of("registrationName", "onInlineViewLayout"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,7 +27,7 @@ public class TextAttributes {
|
||||
private float mLineHeight = Float.NaN;
|
||||
private float mLetterSpacing = Float.NaN;
|
||||
private float mMaxFontSizeMultiplier = Float.NaN;
|
||||
private float mHeightOfTallestInlineImage = Float.NaN;
|
||||
private float mHeightOfTallestInlineViewOrImage = Float.NaN;
|
||||
private TextTransform mTextTransform = TextTransform.UNSET;
|
||||
|
||||
public TextAttributes() {
|
||||
@@ -44,7 +44,7 @@ public class TextAttributes {
|
||||
result.mLineHeight = !Float.isNaN(child.mLineHeight) ? child.mLineHeight : mLineHeight;
|
||||
result.mLetterSpacing = !Float.isNaN(child.mLetterSpacing) ? child.mLetterSpacing : mLetterSpacing;
|
||||
result.mMaxFontSizeMultiplier = !Float.isNaN(child.mMaxFontSizeMultiplier) ? child.mMaxFontSizeMultiplier : mMaxFontSizeMultiplier;
|
||||
result.mHeightOfTallestInlineImage = !Float.isNaN(child.mHeightOfTallestInlineImage) ? child.mHeightOfTallestInlineImage : mHeightOfTallestInlineImage;
|
||||
result.mHeightOfTallestInlineViewOrImage = !Float.isNaN(child.mHeightOfTallestInlineViewOrImage) ? child.mHeightOfTallestInlineViewOrImage : mHeightOfTallestInlineViewOrImage;
|
||||
result.mTextTransform = child.mTextTransform != TextTransform.UNSET ? child.mTextTransform : mTextTransform;
|
||||
|
||||
return result;
|
||||
@@ -96,12 +96,12 @@ public class TextAttributes {
|
||||
mMaxFontSizeMultiplier = maxFontSizeMultiplier;
|
||||
}
|
||||
|
||||
public float getHeightOfTallestInlineImage() {
|
||||
return mHeightOfTallestInlineImage;
|
||||
public float getHeightOfTallestInlineViewOrImage() {
|
||||
return mHeightOfTallestInlineViewOrImage;
|
||||
}
|
||||
|
||||
public void setHeightOfTallestInlineImage(float value) {
|
||||
mHeightOfTallestInlineImage = value;
|
||||
public void setHeightOfTallestInlineViewOrImage(float value) {
|
||||
mHeightOfTallestInlineViewOrImage = value;
|
||||
}
|
||||
|
||||
public TextTransform getTextTransform() {
|
||||
@@ -137,9 +137,9 @@ public class TextAttributes {
|
||||
// Take into account the requested line height
|
||||
// and the height of the inline images.
|
||||
boolean useInlineViewHeight =
|
||||
!Float.isNaN(mHeightOfTallestInlineImage)
|
||||
&& mHeightOfTallestInlineImage > lineHeight;
|
||||
return useInlineViewHeight ? mHeightOfTallestInlineImage : lineHeight;
|
||||
!Float.isNaN(mHeightOfTallestInlineViewOrImage)
|
||||
&& mHeightOfTallestInlineViewOrImage > lineHeight;
|
||||
return useInlineViewHeight ? mHeightOfTallestInlineViewOrImage : lineHeight;
|
||||
}
|
||||
|
||||
public float getEffectiveLetterSpacing() {
|
||||
@@ -169,7 +169,7 @@ public class TextAttributes {
|
||||
+ "\n getAllowFontScaling(): " + getAllowFontScaling()
|
||||
+ "\n getFontSize(): " + getFontSize()
|
||||
+ "\n getEffectiveFontSize(): " + getEffectiveFontSize()
|
||||
+ "\n getHeightOfTallestInlineImage(): " + getHeightOfTallestInlineImage()
|
||||
+ "\n getHeightOfTallestInlineViewOrImage(): " + getHeightOfTallestInlineViewOrImage()
|
||||
+ "\n getLetterSpacing(): " + getLetterSpacing()
|
||||
+ "\n getEffectiveLetterSpacing(): " + getEffectiveLetterSpacing()
|
||||
+ "\n getLineHeight(): " + getLineHeight()
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.text;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
/**
|
||||
* TextInlineViewPlaceholderSpan is a span for inlined views that are inside <Text/>. It computes
|
||||
* its size based on the input size. It contains no draw logic, just positioning logic.
|
||||
*/
|
||||
public class TextInlineViewPlaceholderSpan extends ReplacementSpan implements ReactSpan {
|
||||
private int mReactTag;
|
||||
private int mWidth;
|
||||
private int mHeight;
|
||||
|
||||
public TextInlineViewPlaceholderSpan(int reactTag, int width, int height) {
|
||||
mReactTag = reactTag;
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
}
|
||||
|
||||
public int getReactTag() {
|
||||
return mReactTag;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(
|
||||
Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
|
||||
// NOTE: This getSize code is copied from DynamicDrawableSpan and modified to not use a Drawable
|
||||
|
||||
if (fm != null) {
|
||||
fm.ascent = -mHeight;
|
||||
fm.descent = 0;
|
||||
|
||||
fm.top = fm.ascent;
|
||||
fm.bottom = 0;
|
||||
}
|
||||
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(
|
||||
Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.uimanager.LayoutShadowNode;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactShadowNodeImpl;
|
||||
import com.facebook.react.uimanager.Spacing;
|
||||
@@ -196,7 +197,12 @@ public class ReactTextInputShadowNode extends ReactBaseTextShadowNode
|
||||
if (mMostRecentEventCount != UNSET) {
|
||||
ReactTextUpdate reactTextUpdate =
|
||||
new ReactTextUpdate(
|
||||
spannedFromShadowNode(this, getText()),
|
||||
spannedFromShadowNode(
|
||||
this,
|
||||
getText(),
|
||||
/* supportsInlineViews: */ false,
|
||||
/* nativeViewHierarchyOptimizer: */ null // only needed to support inline views
|
||||
),
|
||||
mMostRecentEventCount,
|
||||
mContainsImages,
|
||||
getPadding(Spacing.LEFT),
|
||||
|
||||
Reference in New Issue
Block a user