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
This commit is contained in:
parent
492412f177
commit
a0268a7bfc
|
@ -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);
|
||||
|
||||
|
@ -146,10 +150,14 @@ var Image = React.createClass({
|
|||
{this.props.children}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
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.scroll.ReactScrollViewManager;
|
|||
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 @@ public class MainReactPackage implements ReactPackage {
|
|||
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 javax.annotation.Nullable;
|
|||
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 @@ import com.facebook.react.uimanager.ViewProps;
|
|||
*/
|
||||
public class ReactTextShadowNode extends LayoutShadowNode {
|
||||
|
||||
private static final String INLINE_IMAGE_PLACEHOLDER = "I";
|
||||
public static final int UNSET = -1;
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -98,11 +100,13 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
|||
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 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
|||
}
|
||||
}
|
||||
|
||||
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 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
|||
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 class ReactTextShadowNode extends LayoutShadowNode {
|
|||
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 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
|||
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 class ReactTextShadowNode extends LayoutShadowNode {
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
package com.facebook.react.views.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
@ -18,10 +19,17 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||
|
||||
public class ReactTextView extends TextView implements ReactCompoundView {
|
||||
|
||||
private boolean mContainsImages;
|
||||
|
||||
public ReactTextView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void setText(ReactTextUpdate update) {
|
||||
mContainsImages = update.containsImages();
|
||||
setText(update.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int reactTagForTouch(float touchX, float touchY) {
|
||||
Spanned text = (Spanned) getText();
|
||||
|
@ -61,4 +69,80 @@ public class ReactTextView extends TextView implements ReactCompoundView {
|
|||
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean verifyDrawable(Drawable drawable) {
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
if (span.getDrawable() == drawable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.verifyDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateDrawable(Drawable drawable) {
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
if (span.getDrawable() == drawable) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
super.invalidateDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onDetachedFromWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTemporaryDetach() {
|
||||
super.onStartTemporaryDetach();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onStartTemporaryDetach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onAttachedToWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinishTemporaryDetach() {
|
||||
super.onFinishTemporaryDetach();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onFinishTemporaryDetach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ package com.facebook.react.views.text;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.widget.TextView;
|
||||
|
@ -82,7 +81,12 @@ public class ReactTextViewManager extends BaseViewManager<ReactTextView, ReactTe
|
|||
|
||||
@Override
|
||||
public void updateExtraData(ReactTextView view, Object extraData) {
|
||||
view.setText((Spanned) extraData);
|
||||
ReactTextUpdate update = (ReactTextUpdate) extraData;
|
||||
if (update.containsImages()) {
|
||||
Spannable spannable = update.getText();
|
||||
TextInlineImageSpan.possiblyUpdateInlineImageSpans(spannable, view);
|
||||
}
|
||||
view.setText(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* 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 android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.Spannable;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchy;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.interfaces.DraweeController;
|
||||
import com.facebook.drawee.view.DraweeHolder;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
|
||||
/**
|
||||
* TextInlineImageSpan is a span for Images that are inside <Text/>. It computes it's size based
|
||||
* on the input size. When it is time to draw, it will use the Fresco framework to get the right
|
||||
* Drawable and let that draw.
|
||||
*
|
||||
* Since Fresco needs to callback to the TextView that contains this, in the ViewManager, you must
|
||||
* tell the Span about the TextView
|
||||
*
|
||||
* Note: It borrows code from DynamicDrawableSpan and if that code updates how it computes size or
|
||||
* draws, we need to update this as well.
|
||||
*/
|
||||
public class TextInlineImageSpan extends ReplacementSpan {
|
||||
|
||||
private @Nullable Drawable mDrawable;
|
||||
private DraweeHolder<GenericDraweeHierarchy> mDraweeHolder;
|
||||
|
||||
private int mHeight;
|
||||
private Uri mUri;
|
||||
private int mWidth;
|
||||
|
||||
private @Nullable TextView mTextView;
|
||||
|
||||
public TextInlineImageSpan(Resources resources, int height, int width, @Nullable Uri uri) {
|
||||
mDraweeHolder = new DraweeHolder(
|
||||
GenericDraweeHierarchyBuilder.newInstance(resources)
|
||||
.build()
|
||||
);
|
||||
|
||||
mHeight = height;
|
||||
mWidth = width;
|
||||
mUri = (uri != null) ? uri : Uri.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ReactTextView that holds this ImageSpan is responsible for passing these methods on so
|
||||
* that we can do proper lifetime management for Fresco
|
||||
*/
|
||||
public void onDetachedFromWindow() {
|
||||
mDraweeHolder.onDetach();
|
||||
}
|
||||
|
||||
public void onStartTemporaryDetach() {
|
||||
mDraweeHolder.onDetach();
|
||||
}
|
||||
|
||||
public void onAttachedToWindow() {
|
||||
mDraweeHolder.onAttach();
|
||||
}
|
||||
|
||||
public void onFinishTemporaryDetach() {
|
||||
mDraweeHolder.onAttach();
|
||||
}
|
||||
|
||||
public @Nullable Drawable getDrawable() {
|
||||
return mDrawable;
|
||||
}
|
||||
|
||||
@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) {
|
||||
if (mDrawable == null) {
|
||||
ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mUri)
|
||||
.build();
|
||||
|
||||
DraweeController draweeController = Fresco.newDraweeControllerBuilder()
|
||||
.setOldController(mDraweeHolder.getController())
|
||||
.setImageRequest(imageRequest)
|
||||
.build();
|
||||
mDraweeHolder.setController(draweeController);
|
||||
|
||||
mDrawable = mDraweeHolder.getTopLevelDrawable();
|
||||
mDrawable.setBounds(0, 0, mWidth, mHeight);
|
||||
mDrawable.setCallback(mTextView);
|
||||
}
|
||||
|
||||
// NOTE: This drawing code is copied from DynamicDrawableSpan
|
||||
|
||||
canvas.save();
|
||||
|
||||
int transY = bottom - mDrawable.getBounds().bottom;
|
||||
|
||||
canvas.translate(x, transY);
|
||||
mDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* For TextInlineImageSpan we need to update the Span to know that the window is attached and
|
||||
* the TextView that we will set as the callback on the Drawable.
|
||||
*
|
||||
* @param spannable The spannable that may contain TextInlineImageSpans
|
||||
* @param view The view which will be set as the callback for the Drawable
|
||||
*/
|
||||
public static void possiblyUpdateInlineImageSpans(Spannable spannable, TextView view) {
|
||||
TextInlineImageSpan[] spans =
|
||||
spannable.getSpans(0, spannable.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onAttachedToWindow();
|
||||
span.mTextView = view;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -15,6 +15,7 @@ import java.util.ArrayList;
|
|||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
@ -31,6 +32,8 @@ import android.widget.EditText;
|
|||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.views.text.CustomStyleSpan;
|
||||
import com.facebook.react.views.text.ReactTagSpan;
|
||||
import com.facebook.react.views.text.ReactTextUpdate;
|
||||
import com.facebook.react.views.text.TextInlineImageSpan;
|
||||
|
||||
/**
|
||||
* A wrapper around the EditText that lets us better control what happens when an EditText gets
|
||||
|
@ -60,6 +63,7 @@ public class ReactEditText extends EditText {
|
|||
private @Nullable ArrayList<TextWatcher> mListeners;
|
||||
private @Nullable TextWatcherDelegator mTextWatcherDelegator;
|
||||
private int mStagedInputType;
|
||||
private boolean mContainsImages;
|
||||
|
||||
public ReactEditText(Context context) {
|
||||
super(context);
|
||||
|
@ -195,6 +199,7 @@ public class ReactEditText extends EditText {
|
|||
SpannableStringBuilder spannableStringBuilder =
|
||||
new SpannableStringBuilder(reactTextUpdate.getText());
|
||||
manageSpans(spannableStringBuilder);
|
||||
mContainsImages = reactTextUpdate.containsImages();
|
||||
mIsSettingTextFromJS = true;
|
||||
getText().replace(0, length(), spannableStringBuilder);
|
||||
mIsSettingTextFromJS = false;
|
||||
|
@ -283,6 +288,82 @@ public class ReactEditText extends EditText {
|
|||
setGravity((getGravity() & ~Gravity.VERTICAL_GRAVITY_MASK) | gravityVertical);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean verifyDrawable(Drawable drawable) {
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
if (span.getDrawable() == drawable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.verifyDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateDrawable(Drawable drawable) {
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
if (span.getDrawable() == drawable) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
super.invalidateDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onDetachedFromWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTemporaryDetach() {
|
||||
super.onStartTemporaryDetach();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onStartTemporaryDetach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onAttachedToWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinishTemporaryDetach() {
|
||||
super.onFinishTemporaryDetach();
|
||||
if (mContainsImages && getText() instanceof Spanned) {
|
||||
Spanned text = (Spanned) getText();
|
||||
TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class);
|
||||
for (TextInlineImageSpan span : spans) {
|
||||
span.onFinishTemporaryDetach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class will redirect *TextChanged calls to the listeners only in the case where the text
|
||||
* is changed by the user, and not explicitly set by JS.
|
||||
|
|
|
@ -17,6 +17,7 @@ import android.graphics.PorterDuff;
|
|||
import android.os.SystemClock;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
|
@ -40,6 +41,8 @@ import com.facebook.react.uimanager.ViewDefaults;
|
|||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.views.text.DefaultStyleValuesUtil;
|
||||
import com.facebook.react.views.text.ReactTextUpdate;
|
||||
import com.facebook.react.views.text.TextInlineImageSpan;
|
||||
|
||||
/**
|
||||
* Manages instances of TextInput.
|
||||
|
@ -147,7 +150,12 @@ public class ReactTextInputManager extends
|
|||
(int) Math.ceil(padding[2]),
|
||||
(int) Math.ceil(padding[3]));
|
||||
} else if (extraData instanceof ReactTextUpdate) {
|
||||
view.maybeSetText((ReactTextUpdate) extraData);
|
||||
ReactTextUpdate update = (ReactTextUpdate) extraData;
|
||||
if (update.containsImages()) {
|
||||
Spannable spannable = update.getText();
|
||||
TextInlineImageSpan.possiblyUpdateInlineImageSpans(spannable, view);
|
||||
}
|
||||
view.maybeSetText(update);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ package com.facebook.react.views.textinput;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.Spannable;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -28,6 +28,7 @@ import com.facebook.react.uimanager.ThemedReactContext;
|
|||
import com.facebook.react.uimanager.UIViewOperationQueue;
|
||||
import com.facebook.react.uimanager.ViewDefaults;
|
||||
import com.facebook.react.views.text.ReactTextShadowNode;
|
||||
import com.facebook.react.views.text.ReactTextUpdate;
|
||||
|
||||
@VisibleForTesting
|
||||
public class ReactTextInputShadowNode extends ReactTextShadowNode implements
|
||||
|
@ -111,8 +112,9 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
|
|||
}
|
||||
|
||||
if (mJsEventCount != UNSET) {
|
||||
Spanned preparedSpannedText = fromTextCSSNode(this);
|
||||
ReactTextUpdate reactTextUpdate = new ReactTextUpdate(preparedSpannedText, mJsEventCount);
|
||||
Spannable preparedSpannableText = fromTextCSSNode(this);
|
||||
ReactTextUpdate reactTextUpdate =
|
||||
new ReactTextUpdate(preparedSpannableText, mJsEventCount, mContainsImages);
|
||||
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue