Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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 authored and facebook-github-bot-0 committed Nov 13, 2015
1 parent 492412f commit a0268a7
Show file tree
Hide file tree
Showing 13 changed files with 544 additions and 20 deletions.
6 changes: 6 additions & 0 deletions Examples/UIExplorer/TextExample.android.js
Expand Up @@ -17,6 +17,7 @@

var React = require('react-native');
var {
Image,
StyleSheet,
Text,
View,
Expand Down Expand Up @@ -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>
);
}
Expand Down
14 changes: 13 additions & 1 deletion Libraries/Image/Image.android.js
Expand Up @@ -115,6 +115,10 @@ var Image = React.createClass({
this._updateViewConfig(nextProps);
},

contextTypes: {
isInAParentText: React.PropTypes.bool
},

render: function() {
var source = resolveAssetSource(this.props.source);

Expand Down Expand Up @@ -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;
Expand All @@ -171,5 +179,9 @@ var RKImage = createReactNativeComponentClass({
validAttributes: ImageViewAttributes,
uiViewClassName: 'RCTImageView',
});
var RCTTextInlineImage = createReactNativeComponentClass({
validAttributes: ImageViewAttributes,
uiViewClassName: 'RCTTextInlineImage',
});

module.exports = Image;
Expand Up @@ -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;
Expand Down Expand Up @@ -74,6 +75,7 @@ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext
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) {
}

}
Expand Up @@ -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;
Expand Down Expand Up @@ -56,6 +57,7 @@
*/
public class ReactTextShadowNode extends LayoutShadowNode {

private static final String INLINE_IMAGE_PLACEHOLDER = "I";
public static final int UNSET = -1;

@VisibleForTesting
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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

Expand All @@ -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;
Expand All @@ -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 ?
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
Expand Up @@ -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;
}
}

0 comments on commit a0268a7

Please sign in to comment.