Permalink
Browse files

Initial checkin for Image within Text nodes

Summary: This adds the basic support for embedding an image in a TextView.

Implementation details :

We create a ReactTextInlineImageShadowNode whenever an Image is embedded within a Text context.
That uses the same parsing code as ReactImageView (copied, not shared) to parse the source property to figure out the Uri where the resource is.

In ReactTextShadowNode we now look for the ReactTextInlineImageShadowNode and place a TextInlineImageSpan so that we can layout appropriately

Later at the time we go to setText on the TextView, we update that TextInlineImageSpan so that the proper Drawable (downloaded via Fresco) can be shown

public

Reviewed By: mkonicek

Differential Revision: D2652667

fb-gh-sync-id: 8f24924d204f78b8bc4d5d67835cc73b3c1859dd
  • Loading branch information...
Dave Miller facebook-github-bot-0
Dave Miller authored and facebook-github-bot-0 committed Nov 13, 2015
1 parent 492412f commit a0268a7bfc8000b5297d2b50f81e000d1f479c76
@@ -17,6 +17,7 @@
var React = require('react-native');
var {
+ Image,
StyleSheet,
Text,
View,
@@ -359,6 +360,11 @@ var TextExample = React.createClass({
No maximum lines specified no matter now much I write here. If I keep writing it{"'"}ll just keep going and going
</Text>
</UIExplorerBlock>
+ <UIExplorerBlock title="Inline images">
+ <Text>
+ This text contains an inline image <Image source={require('./flux.png')}/>. Neat, huh?
+ </Text>
+ </UIExplorerBlock>
</UIExplorerPage>
);
}
@@ -115,6 +115,10 @@ var Image = React.createClass({
this._updateViewConfig(nextProps);
},
+ contextTypes: {
+ isInAParentText: React.PropTypes.bool
+ },
+
render: function() {
var source = resolveAssetSource(this.props.source);
@@ -147,7 +151,11 @@ var Image = React.createClass({
</View>
);
} else {
- return <RKImage {...nativeProps}/>;
+ if (this.context.isInAParentText) {
+ return <RCTTextInlineImage {...nativeProps}/>;
+ } else {
+ return <RKImage {...nativeProps}/>;
+ }
}
}
return null;
@@ -171,5 +179,9 @@ var RKImage = createReactNativeComponentClass({
validAttributes: ImageViewAttributes,
uiViewClassName: 'RCTImageView',
});
+var RCTTextInlineImage = createReactNativeComponentClass({
+ validAttributes: ImageViewAttributes,
+ uiViewClassName: 'RCTTextInlineImage',
+});
module.exports = Image;
@@ -32,6 +32,7 @@
import com.facebook.react.views.switchview.ReactSwitchManager;
import com.facebook.react.views.text.ReactRawTextManager;
import com.facebook.react.views.text.ReactTextViewManager;
+import com.facebook.react.views.text.ReactTextInlineImageViewManager;
import com.facebook.react.views.text.ReactVirtualTextViewManager;
import com.facebook.react.views.textinput.ReactTextInputManager;
import com.facebook.react.views.toolbar.ReactToolbarManager;
@@ -74,6 +75,7 @@
new ReactToolbarManager(),
new ReactViewManager(),
new ReactViewPagerManager(),
+ new ReactTextInlineImageViewManager(),
new ReactVirtualTextViewManager());
}
}
@@ -0,0 +1,80 @@
+/**
+ * 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.views.text;
+
+import javax.annotation.Nullable;
+
+import java.util.Locale;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.facebook.common.util.UriUtil;
+import com.facebook.react.uimanager.LayoutShadowNode;
+import com.facebook.react.uimanager.ReactProp;
+import com.facebook.react.uimanager.ReactShadowNode;
+
+/**
+ * {@link ReactShadowNode} class for Image embedded within a TextView.
+ *
+ */
+public class ReactTextInlineImageShadowNode extends LayoutShadowNode {
+
+ private @Nullable Uri mUri;
+
+ @ReactProp(name = "src")
+ public void setSource(@Nullable String source) {
+ Uri uri = null;
+ if (source != null) {
+ try {
+ uri = Uri.parse(source);
+ // Verify scheme is set, so that relative uri (used by static resources) are not handled.
+ if (uri.getScheme() == null) {
+ uri = null;
+ }
+ } catch (Exception e) {
+ // ignore malformed uri, then attempt to extract resource ID.
+ }
+ if (uri == null) {
+ uri = getResourceDrawableUri(getThemedContext(), source);
+ }
+ }
+ if (uri != mUri) {
+ markUpdated();
+ }
+ mUri = uri;
+ }
+
+ public @Nullable Uri getUri() {
+ return mUri;
+ }
+
+ // TODO: t9053573 is tracking that this code should be shared
+ private static @Nullable Uri getResourceDrawableUri(Context context, @Nullable String name) {
+ if (name == null || name.isEmpty()) {
+ return null;
+ }
+ name = name.toLowerCase(Locale.getDefault()).replace("-", "_");
+ int resId = context.getResources().getIdentifier(
+ name,
+ "drawable",
+ context.getPackageName());
+ return new Uri.Builder()
+ .scheme(UriUtil.LOCAL_RESOURCE_SCHEME)
+ .path(String.valueOf(resId))
+ .build();
+ }
+
+ @Override
+ public boolean isVirtual() {
+ return true;
+ }
+
+}
@@ -0,0 +1,50 @@
+/**
+ * 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.views.text;
+
+import android.view.View;
+
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewManager;
+
+/**
+ * Manages Images embedded in Text nodes. Since they are used only as a virtual nodes any type of
+ * native view operation will throw an {@link IllegalStateException}
+ */
+public class ReactTextInlineImageViewManager
+ extends ViewManager<View, ReactTextInlineImageShadowNode> {
+
+ static final String REACT_CLASS = "RCTTextInlineImage";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public View createViewInstance(ThemedReactContext context) {
+ throw new IllegalStateException("RCTTextInlineImage doesn't map into a native view");
+ }
+
+ @Override
+ public ReactTextInlineImageShadowNode createShadowNodeInstance() {
+ return new ReactTextInlineImageShadowNode();
+ }
+
+ @Override
+ public Class<ReactTextInlineImageShadowNode> getShadowNodeClass() {
+ return ReactTextInlineImageShadowNode.class;
+ }
+
+ @Override
+ public void updateExtraData(View root, Object extraData) {
+ }
+
+}
@@ -14,6 +14,7 @@
import java.util.ArrayList;
import java.util.List;
+import android.content.res.Resources;
import android.graphics.Typeface;
import android.text.BoringLayout;
import android.text.Layout;
@@ -56,6 +57,7 @@
*/
public class ReactTextShadowNode extends LayoutShadowNode {
+ private static final String INLINE_IMAGE_PLACEHOLDER = "I";
public static final int UNSET = -1;
@VisibleForTesting
@@ -98,11 +100,13 @@ private static final void buildSpannedFromTextCSSNode(
CSSNode child = textCSSNode.getChildAt(i);
if (child instanceof ReactTextShadowNode) {
buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
+ } else if (child instanceof ReactTextInlineImageShadowNode) {
+ buildSpannedFromImageNode((ReactTextInlineImageShadowNode) child, sb, ops);
} else {
throw new IllegalViewOperationException("Unexpected view type nested under text node: "
+ child.getClass());
}
- ((ReactTextShadowNode) child).markUpdateSeen();
+ ((ReactShadowNode) child).markUpdateSeen();
}
int end = sb.length();
if (end >= start) {
@@ -135,7 +139,24 @@ private static final void buildSpannedFromTextCSSNode(
}
}
- protected static final Spanned fromTextCSSNode(ReactTextShadowNode textCSSNode) {
+ private static final void buildSpannedFromImageNode(
+ ReactTextInlineImageShadowNode node,
+ SpannableStringBuilder sb,
+ List<SetSpanOperation> ops) {
+ int start = sb.length();
+ // Create our own internal ImageSpan which will allow us to correctly layout the Image
+ Resources resources = node.getThemedContext().getResources();
+ int height = (int) PixelUtil.toDIPFromPixel(node.getStyleHeight());
+ int width = (int) PixelUtil.toDIPFromPixel(node.getStyleWidth());
+ TextInlineImageSpan imageSpan =
+ new TextInlineImageSpan(resources, height, width, node.getUri());
+ // 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);
+ ops.add(new SetSpanOperation(start, sb.length(), imageSpan));
+ }
+
+ protected static final Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) {
SpannableStringBuilder sb = new SpannableStringBuilder();
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
@@ -151,8 +172,15 @@ protected static final Spanned fromTextCSSNode(ReactTextShadowNode textCSSNode)
sb.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
+
+ textCSSNode.mContainsImages = false;
+
+ // While setting the Spans on the final text, we also check whether any of them are images
for (int i = ops.size() - 1; i >= 0; i--) {
SetSpanOperation op = ops.get(i);
+ if (op.what instanceof TextInlineImageSpan) {
+ textCSSNode.mContainsImages = true;
+ }
op.execute(sb);
}
return sb;
@@ -167,7 +195,7 @@ public void measure(CSSNode node, float width, MeasureOutput measureOutput) {
TextPaint textPaint = sTextPaintInstance;
Layout layout;
Spanned text = Assertions.assertNotNull(
- reactCSSNode.mPreparedSpannedText,
+ reactCSSNode.mPreparedSpannableText,
"Spannable element has not been prepared in onBeforeLayout");
BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
float desiredWidth = boring == null ?
@@ -272,15 +300,17 @@ private static int parseNumericFontWeight(String fontWeightString) {
private @Nullable String mFontFamily = null;
private @Nullable String mText = null;
- private @Nullable Spanned mPreparedSpannedText;
+ private @Nullable Spannable mPreparedSpannableText;
private final boolean mIsVirtual;
+ protected boolean mContainsImages = false;
+
@Override
public void onBeforeLayout() {
if (mIsVirtual) {
return;
}
- mPreparedSpannedText = fromTextCSSNode(this);
+ mPreparedSpannableText = fromTextCSSNode(this);
markUpdated();
}
@@ -394,8 +424,10 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
return;
}
super.onCollectExtraUpdates(uiViewOperationQueue);
- if (mPreparedSpannedText != null) {
- uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), mPreparedSpannedText);
+ if (mPreparedSpannableText != null) {
+ ReactTextUpdate reactTextUpdate =
+ new ReactTextUpdate(mPreparedSpannableText, UNSET, mContainsImages);
+ uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
}
}
@@ -7,29 +7,36 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
-package com.facebook.react.views.textinput;
+package com.facebook.react.views.text;
-import android.text.Spanned;
+import android.text.Spannable;
/**
- * Class that contains the data needed for a Text Input text update.
+ * Class that contains the data needed for a text update.
+ * Used by both <Text/> and <TextInput/>
* VisibleForTesting from {@link TextInputEventsTestCase}.
*/
public class ReactTextUpdate {
- private final Spanned mText;
+ private final Spannable mText;
private final int mJsEventCounter;
+ private final boolean mContainsImages;
- public ReactTextUpdate(Spanned text, int jsEventCounter) {
+ public ReactTextUpdate(Spannable text, int jsEventCounter, boolean containsImages) {
mText = text;
mJsEventCounter = jsEventCounter;
+ mContainsImages = containsImages;
}
- public Spanned getText() {
+ public Spannable getText() {
return mText;
}
public int getJsEventCounter() {
return mJsEventCounter;
}
+
+ public boolean containsImages() {
+ return mContainsImages;
+ }
}
Oops, something went wrong.

0 comments on commit a0268a7

Please sign in to comment.