decouple textview from fresco
Reviewed By: andreicoman11 Differential Revision: D2960626 fb-gh-sync-id: c03aa7f16cdea795cefe39da2c5d660ae6278a37 shipit-source-id: c03aa7f16cdea795cefe39da2c5d660ae6278a37
This commit is contained in:
parent
c32e5fd84f
commit
9baef48498
|
@ -19,6 +19,7 @@ android_library(
|
||||||
react_native_target('java/com/facebook/react/views/swiperefresh:swiperefresh'),
|
react_native_target('java/com/facebook/react/views/swiperefresh:swiperefresh'),
|
||||||
react_native_target('java/com/facebook/react/views/switchview:switchview'),
|
react_native_target('java/com/facebook/react/views/switchview:switchview'),
|
||||||
react_native_target('java/com/facebook/react/views/text:text'),
|
react_native_target('java/com/facebook/react/views/text:text'),
|
||||||
|
react_native_target('java/com/facebook/react/views/text/frescosupport:frescosupport'),
|
||||||
react_native_target('java/com/facebook/react/views/textinput:textinput'),
|
react_native_target('java/com/facebook/react/views/textinput:textinput'),
|
||||||
react_native_target('java/com/facebook/react/views/toolbar:toolbar'),
|
react_native_target('java/com/facebook/react/views/toolbar:toolbar'),
|
||||||
react_native_target('java/com/facebook/react/views/view:view'),
|
react_native_target('java/com/facebook/react/views/view:view'),
|
||||||
|
|
|
@ -22,8 +22,8 @@ import com.facebook.react.modules.camera.CameraRollManager;
|
||||||
import com.facebook.react.modules.camera.ImageEditingManager;
|
import com.facebook.react.modules.camera.ImageEditingManager;
|
||||||
import com.facebook.react.modules.camera.ImageStoreManager;
|
import com.facebook.react.modules.camera.ImageStoreManager;
|
||||||
import com.facebook.react.modules.clipboard.ClipboardModule;
|
import com.facebook.react.modules.clipboard.ClipboardModule;
|
||||||
import com.facebook.react.modules.dialog.DialogModule;
|
|
||||||
import com.facebook.react.modules.datepicker.DatePickerDialogModule;
|
import com.facebook.react.modules.datepicker.DatePickerDialogModule;
|
||||||
|
import com.facebook.react.modules.dialog.DialogModule;
|
||||||
import com.facebook.react.modules.fresco.FrescoModule;
|
import com.facebook.react.modules.fresco.FrescoModule;
|
||||||
import com.facebook.react.modules.intent.IntentModule;
|
import com.facebook.react.modules.intent.IntentModule;
|
||||||
import com.facebook.react.modules.location.LocationModule;
|
import com.facebook.react.modules.location.LocationModule;
|
||||||
|
@ -45,16 +45,16 @@ import com.facebook.react.views.progressbar.ReactProgressBarViewManager;
|
||||||
import com.facebook.react.views.recyclerview.RecyclerViewBackedScrollViewManager;
|
import com.facebook.react.views.recyclerview.RecyclerViewBackedScrollViewManager;
|
||||||
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
|
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
|
||||||
import com.facebook.react.views.scroll.ReactScrollViewManager;
|
import com.facebook.react.views.scroll.ReactScrollViewManager;
|
||||||
|
import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager;
|
||||||
import com.facebook.react.views.switchview.ReactSwitchManager;
|
import com.facebook.react.views.switchview.ReactSwitchManager;
|
||||||
import com.facebook.react.views.text.ReactRawTextManager;
|
import com.facebook.react.views.text.ReactRawTextManager;
|
||||||
import com.facebook.react.views.text.ReactTextViewManager;
|
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.text.ReactVirtualTextViewManager;
|
||||||
|
import com.facebook.react.views.textfrescosupport.FrescoBasedReactTextInlineImageViewManager;
|
||||||
import com.facebook.react.views.textinput.ReactTextInputManager;
|
import com.facebook.react.views.textinput.ReactTextInputManager;
|
||||||
import com.facebook.react.views.toolbar.ReactToolbarManager;
|
import com.facebook.react.views.toolbar.ReactToolbarManager;
|
||||||
import com.facebook.react.views.view.ReactViewManager;
|
import com.facebook.react.views.view.ReactViewManager;
|
||||||
import com.facebook.react.views.viewpager.ReactViewPagerManager;
|
import com.facebook.react.views.viewpager.ReactViewPagerManager;
|
||||||
import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager;
|
|
||||||
import com.facebook.react.views.webview.ReactWebViewManager;
|
import com.facebook.react.views.webview.ReactWebViewManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,7 +106,7 @@ public class MainReactPackage implements ReactPackage {
|
||||||
new ReactRawTextManager(),
|
new ReactRawTextManager(),
|
||||||
new ReactScrollViewManager(),
|
new ReactScrollViewManager(),
|
||||||
new ReactSwitchManager(),
|
new ReactSwitchManager(),
|
||||||
new ReactTextInlineImageViewManager(),
|
new FrescoBasedReactTextInlineImageViewManager(),
|
||||||
new ReactTextInputManager(),
|
new ReactTextInputManager(),
|
||||||
new ReactTextViewManager(),
|
new ReactTextViewManager(),
|
||||||
new ReactToolbarManager(),
|
new ReactToolbarManager(),
|
||||||
|
|
|
@ -9,10 +9,6 @@ android_library(
|
||||||
react_native_target('java/com/facebook/csslayout:csslayout'),
|
react_native_target('java/com/facebook/csslayout:csslayout'),
|
||||||
react_native_target('java/com/facebook/react/uimanager:uimanager'),
|
react_native_target('java/com/facebook/react/uimanager:uimanager'),
|
||||||
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
|
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
|
||||||
react_native_dep('libraries/fresco/fresco-react-native:fbcore'),
|
|
||||||
react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'),
|
|
||||||
react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'),
|
|
||||||
react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'),
|
|
||||||
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
|
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
|
||||||
react_native_dep('third-party/java/jsr-305:jsr-305'),
|
react_native_dep('third-party/java/jsr-305:jsr-305'),
|
||||||
],
|
],
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
package com.facebook.react.views.text;
|
package com.facebook.react.views.text;
|
||||||
|
|
||||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||||
import com.facebook.react.uimanager.ReactStylesDiffMap;
|
|
||||||
import com.facebook.react.uimanager.ThemedReactContext;
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,90 +9,17 @@
|
||||||
|
|
||||||
package com.facebook.react.views.text;
|
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.drawee.controller.AbstractDraweeControllerBuilder;
|
|
||||||
import com.facebook.react.uimanager.LayoutShadowNode;
|
import com.facebook.react.uimanager.LayoutShadowNode;
|
||||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
||||||
import com.facebook.react.uimanager.ReactShadowNode;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ReactShadowNode} class for Image embedded within a TextView.
|
* Base class for {@link com.facebook.csslayout.CSSNode}s that represent inline images.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class ReactTextInlineImageShadowNode extends LayoutShadowNode {
|
public abstract class ReactTextInlineImageShadowNode extends LayoutShadowNode {
|
||||||
|
|
||||||
private @Nullable Uri mUri;
|
/**
|
||||||
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
|
* Build a {@link TextInlineImageSpan} from this node. This will be added to the TextView in
|
||||||
private final @Nullable Object mCallerContext;
|
* place of this node.
|
||||||
|
*/
|
||||||
public ReactTextInlineImageShadowNode(
|
public abstract TextInlineImageSpan buildInlineImageSpan();
|
||||||
AbstractDraweeControllerBuilder draweeControllerBuilder,
|
|
||||||
@Nullable Object callerContext) {
|
|
||||||
mDraweeControllerBuilder = draweeControllerBuilder;
|
|
||||||
mCallerContext = callerContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractDraweeControllerBuilder getDraweeControllerBuilder() {
|
|
||||||
return mDraweeControllerBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Object getCallerContext() {
|
|
||||||
return mCallerContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import javax.annotation.Nullable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.text.BoringLayout;
|
import android.text.BoringLayout;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
|
@ -37,11 +36,11 @@ import com.facebook.react.common.annotations.VisibleForTesting;
|
||||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||||
import com.facebook.react.uimanager.LayoutShadowNode;
|
import com.facebook.react.uimanager.LayoutShadowNode;
|
||||||
import com.facebook.react.uimanager.PixelUtil;
|
import com.facebook.react.uimanager.PixelUtil;
|
||||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
||||||
import com.facebook.react.uimanager.ReactShadowNode;
|
import com.facebook.react.uimanager.ReactShadowNode;
|
||||||
import com.facebook.react.uimanager.UIViewOperationQueue;
|
import com.facebook.react.uimanager.UIViewOperationQueue;
|
||||||
import com.facebook.react.uimanager.ViewDefaults;
|
import com.facebook.react.uimanager.ViewDefaults;
|
||||||
import com.facebook.react.uimanager.ViewProps;
|
import com.facebook.react.uimanager.ViewProps;
|
||||||
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ReactShadowNode} class for spannable text view.
|
* {@link ReactShadowNode} class for spannable text view.
|
||||||
|
@ -94,7 +93,7 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final void buildSpannedFromTextCSSNode(
|
private static void buildSpannedFromTextCSSNode(
|
||||||
ReactTextShadowNode textCSSNode,
|
ReactTextShadowNode textCSSNode,
|
||||||
SpannableStringBuilder sb,
|
SpannableStringBuilder sb,
|
||||||
List<SetSpanOperation> ops) {
|
List<SetSpanOperation> ops) {
|
||||||
|
@ -107,7 +106,14 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
||||||
if (child instanceof ReactTextShadowNode) {
|
if (child instanceof ReactTextShadowNode) {
|
||||||
buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
|
buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
|
||||||
} else if (child instanceof ReactTextInlineImageShadowNode) {
|
} else if (child instanceof ReactTextInlineImageShadowNode) {
|
||||||
buildSpannedFromImageNode((ReactTextInlineImageShadowNode) child, sb, ops);
|
// 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(
|
||||||
|
sb.length() - INLINE_IMAGE_PLACEHOLDER.length(),
|
||||||
|
sb.length(),
|
||||||
|
((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalViewOperationException("Unexpected view type nested under text node: "
|
throw new IllegalViewOperationException("Unexpected view type nested under text node: "
|
||||||
+ child.getClass());
|
+ child.getClass());
|
||||||
|
@ -154,36 +160,14 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final void buildSpannedFromImageNode(
|
protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) {
|
||||||
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) Math.ceil(node.getStyleHeight());
|
|
||||||
int width = (int) Math.ceil(node.getStyleWidth());
|
|
||||||
TextInlineImageSpan imageSpan = new TextInlineImageSpan(
|
|
||||||
resources,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
node.getUri(),
|
|
||||||
node.getDraweeControllerBuilder(),
|
|
||||||
node.getCallerContext());
|
|
||||||
// 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();
|
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||||
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
|
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
|
||||||
|
|
||||||
// The {@link SpannableStringBuilder} implementation require setSpan operation to be called
|
// The {@link SpannableStringBuilder} implementation require setSpan operation to be called
|
||||||
// up-to-bottom, otherwise all the spannables that are withing the region for which one may set
|
// up-to-bottom, otherwise all the spannables that are withing the region for which one may set
|
||||||
// a new spannable will be wiped out
|
// a new spannable will be wiped out
|
||||||
List<SetSpanOperation> ops = new ArrayList<SetSpanOperation>();
|
List<SetSpanOperation> ops = new ArrayList<>();
|
||||||
buildSpannedFromTextCSSNode(textCSSNode, sb, ops);
|
buildSpannedFromTextCSSNode(textCSSNode, sb, ops);
|
||||||
if (textCSSNode.mFontSize == UNSET) {
|
if (textCSSNode.mFontSize == UNSET) {
|
||||||
sb.setSpan(
|
sb.setSpan(
|
||||||
|
@ -330,6 +314,13 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
||||||
|
|
||||||
protected boolean mContainsImages = false;
|
protected boolean mContainsImages = false;
|
||||||
|
|
||||||
|
public ReactTextShadowNode(boolean isVirtual) {
|
||||||
|
mIsVirtual = isVirtual;
|
||||||
|
if (!isVirtual) {
|
||||||
|
setMeasureFunction(TEXT_MEASURE_FUNCTION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBeforeLayout() {
|
public void onBeforeLayout() {
|
||||||
if (mIsVirtual) {
|
if (mIsVirtual) {
|
||||||
|
@ -483,11 +474,4 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
||||||
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
|
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReactTextShadowNode(boolean isVirtual) {
|
|
||||||
mIsVirtual = isVirtual;
|
|
||||||
if (!isVirtual) {
|
|
||||||
setMeasureFunction(TEXT_MEASURE_FUNCTION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,163 +7,64 @@
|
||||||
* of patent rights can be found in the PATENTS file in the same directory.
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.facebook.react.views.text;
|
package com.facebook.react.views.text;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.Canvas;
|
import android.text.Spannable;
|
||||||
import android.graphics.Paint;
|
import android.text.style.ReplacementSpan;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.view.View;
|
||||||
import android.net.Uri;
|
import android.widget.TextView;
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.style.ReplacementSpan;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
/**
|
||||||
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
|
* Base class for inline image spans.
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchy;
|
*/
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
public abstract class TextInlineImageSpan extends ReplacementSpan {
|
||||||
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
|
* For TextInlineImageSpan we need to update the Span to know that the window is attached and
|
||||||
* on the input size. When it is time to draw, it will use the Fresco framework to get the right
|
* the TextView that we will set as the callback on the Drawable.
|
||||||
* Drawable and let that draw.
|
*
|
||||||
*
|
* @param spannable The spannable that may contain TextInlineImageSpans
|
||||||
* Since Fresco needs to callback to the TextView that contains this, in the ViewManager, you must
|
* @param view The view which will be set as the callback for the Drawable
|
||||||
* tell the Span about the TextView
|
*/
|
||||||
*
|
public static void possiblyUpdateInlineImageSpans(Spannable spannable, TextView view) {
|
||||||
* Note: It borrows code from DynamicDrawableSpan and if that code updates how it computes size or
|
TextInlineImageSpan[] spans =
|
||||||
* draws, we need to update this as well.
|
spannable.getSpans(0, spannable.length(), TextInlineImageSpan.class);
|
||||||
*/
|
for (TextInlineImageSpan span : spans) {
|
||||||
public class TextInlineImageSpan extends ReplacementSpan {
|
span.onAttachedToWindow();
|
||||||
|
span.setTextView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private @Nullable Drawable mDrawable;
|
/**
|
||||||
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
|
* Get the drawable that is span represents.
|
||||||
private final DraweeHolder<GenericDraweeHierarchy> mDraweeHolder;
|
*/
|
||||||
private final @Nullable Object mCallerContext;
|
public abstract @Nullable Drawable getDrawable();
|
||||||
|
|
||||||
private int mHeight;
|
/**
|
||||||
private Uri mUri;
|
* Called by the text view from {@link View#onDetachedFromWindow()},
|
||||||
private int mWidth;
|
*/
|
||||||
|
public abstract void onDetachedFromWindow();
|
||||||
|
|
||||||
private @Nullable TextView mTextView;
|
/**
|
||||||
|
* Called by the text view from {@link View#onStartTemporaryDetach()}.
|
||||||
|
*/
|
||||||
|
public abstract void onStartTemporaryDetach();
|
||||||
|
|
||||||
public TextInlineImageSpan(
|
/**
|
||||||
Resources resources,
|
* Called by the text view from {@link View#onAttachedToWindow()}.
|
||||||
int height,
|
*/
|
||||||
int width,
|
public abstract void onAttachedToWindow();
|
||||||
@Nullable Uri uri,
|
|
||||||
AbstractDraweeControllerBuilder draweeControllerBuilder,
|
|
||||||
@Nullable Object callerContext) {
|
|
||||||
mDraweeHolder = new DraweeHolder(
|
|
||||||
GenericDraweeHierarchyBuilder.newInstance(resources)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
mDraweeControllerBuilder = draweeControllerBuilder;
|
|
||||||
mCallerContext = callerContext;
|
|
||||||
|
|
||||||
mHeight = height;
|
/**
|
||||||
mWidth = width;
|
* Called by the text view from {@link View#onFinishTemporaryDetach()}.
|
||||||
mUri = (uri != null) ? uri : Uri.EMPTY;
|
*/
|
||||||
}
|
public abstract void onFinishTemporaryDetach();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ReactTextView that holds this ImageSpan is responsible for passing these methods on so
|
* Set the textview that will contain this span.
|
||||||
* that we can do proper lifetime management for Fresco
|
*/
|
||||||
*/
|
public abstract void setTextView(TextView textView);
|
||||||
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 = mDraweeControllerBuilder
|
|
||||||
.reset()
|
|
||||||
.setOldController(mDraweeHolder.getController())
|
|
||||||
.setCallerContext(mCallerContext)
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
include_defs('//ReactAndroid/DEFS')
|
||||||
|
|
||||||
|
android_library(
|
||||||
|
name = 'frescosupport',
|
||||||
|
srcs = glob(['*.java']),
|
||||||
|
deps = [
|
||||||
|
react_native_target('java/com/facebook/csslayout:csslayout'),
|
||||||
|
react_native_target('java/com/facebook/react/bridge:bridge'),
|
||||||
|
react_native_target('java/com/facebook/react/common:common'),
|
||||||
|
react_native_target('java/com/facebook/react/uimanager:uimanager'),
|
||||||
|
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
|
||||||
|
react_native_target('java/com/facebook/react/views/text:text'),
|
||||||
|
react_native_dep('libraries/fresco/fresco-react-native:fbcore'),
|
||||||
|
react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'),
|
||||||
|
react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'),
|
||||||
|
react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'),
|
||||||
|
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
|
||||||
|
react_native_dep('third-party/java/jsr-305:jsr-305'),
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
'PUBLIC',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
project_config(
|
||||||
|
src_target = ':frescosupport',
|
||||||
|
)
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* 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.textfrescosupport;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.facebook.common.util.UriUtil;
|
||||||
|
import com.facebook.csslayout.CSSNode;
|
||||||
|
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
|
||||||
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||||
|
import com.facebook.react.views.text.ReactTextInlineImageShadowNode;
|
||||||
|
import com.facebook.react.views.text.TextInlineImageSpan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link CSSNode} that represents an inline image. Loading is done using Fresco.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class FrescoBasedReactTextInlineImageShadowNode extends ReactTextInlineImageShadowNode {
|
||||||
|
|
||||||
|
private @Nullable Uri mUri;
|
||||||
|
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
|
||||||
|
private final @Nullable Object mCallerContext;
|
||||||
|
|
||||||
|
public FrescoBasedReactTextInlineImageShadowNode(
|
||||||
|
AbstractDraweeControllerBuilder draweeControllerBuilder,
|
||||||
|
@Nullable Object callerContext) {
|
||||||
|
mDraweeControllerBuilder = draweeControllerBuilder;
|
||||||
|
mCallerContext = callerContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TextInlineImageSpan buildInlineImageSpan() {
|
||||||
|
Resources resources = getThemedContext().getResources();
|
||||||
|
int height = (int) Math.ceil(getStyleHeight());
|
||||||
|
int width = (int) Math.ceil(getStyleWidth());
|
||||||
|
return new FrescoBasedReactTextInlineImageSpan(
|
||||||
|
resources,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
getUri(),
|
||||||
|
getDraweeControllerBuilder(),
|
||||||
|
getCallerContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbstractDraweeControllerBuilder getDraweeControllerBuilder() {
|
||||||
|
return mDraweeControllerBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable Object getCallerContext() {
|
||||||
|
return mCallerContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
/**
|
||||||
|
* 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.textfrescosupport;
|
||||||
|
|
||||||
|
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.widget.TextView;
|
||||||
|
|
||||||
|
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
|
||||||
|
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;
|
||||||
|
import com.facebook.react.views.text.TextInlineImageSpan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FrescoBasedTextInlineImageSpan is a span for Images that are inside <Text/>. It computes
|
||||||
|
* its 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 FrescoBasedReactTextInlineImageSpan extends TextInlineImageSpan {
|
||||||
|
|
||||||
|
private @Nullable Drawable mDrawable;
|
||||||
|
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
|
||||||
|
private final DraweeHolder<GenericDraweeHierarchy> mDraweeHolder;
|
||||||
|
private final @Nullable Object mCallerContext;
|
||||||
|
|
||||||
|
private int mHeight;
|
||||||
|
private Uri mUri;
|
||||||
|
private int mWidth;
|
||||||
|
|
||||||
|
private @Nullable TextView mTextView;
|
||||||
|
|
||||||
|
public FrescoBasedReactTextInlineImageSpan(
|
||||||
|
Resources resources,
|
||||||
|
int height,
|
||||||
|
int width,
|
||||||
|
@Nullable Uri uri,
|
||||||
|
AbstractDraweeControllerBuilder draweeControllerBuilder,
|
||||||
|
@Nullable Object callerContext) {
|
||||||
|
mDraweeHolder = new DraweeHolder(
|
||||||
|
GenericDraweeHierarchyBuilder.newInstance(resources)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
mDraweeControllerBuilder = draweeControllerBuilder;
|
||||||
|
mCallerContext = callerContext;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTextView(TextView textView) {
|
||||||
|
mTextView = textView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = mDraweeControllerBuilder
|
||||||
|
.reset()
|
||||||
|
.setOldController(mDraweeHolder.getController())
|
||||||
|
.setCallerContext(mCallerContext)
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
* of patent rights can be found in the PATENTS file in the same directory.
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.facebook.react.views.text;
|
package com.facebook.react.views.textfrescosupport;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
@ -19,24 +19,24 @@ import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
import com.facebook.react.uimanager.ViewManager;
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages Images embedded in Text nodes. Since they are used only as a virtual nodes any type of
|
* Manages Images embedded in Text nodes using Fresco. Since they are used only as a virtual nodes
|
||||||
* native view operation will throw an {@link IllegalStateException}
|
* any type of native view operation will throw an {@link IllegalStateException}.
|
||||||
*/
|
*/
|
||||||
public class ReactTextInlineImageViewManager
|
public class FrescoBasedReactTextInlineImageViewManager
|
||||||
extends ViewManager<View, ReactTextInlineImageShadowNode> {
|
extends ViewManager<View, FrescoBasedReactTextInlineImageShadowNode> {
|
||||||
|
|
||||||
static final String REACT_CLASS = "RCTTextInlineImage";
|
static final String REACT_CLASS = "RCTTextInlineImage";
|
||||||
|
|
||||||
private final @Nullable AbstractDraweeControllerBuilder mDraweeControllerBuilder;
|
private final @Nullable AbstractDraweeControllerBuilder mDraweeControllerBuilder;
|
||||||
private final @Nullable Object mCallerContext;
|
private final @Nullable Object mCallerContext;
|
||||||
|
|
||||||
public ReactTextInlineImageViewManager() {
|
public FrescoBasedReactTextInlineImageViewManager() {
|
||||||
this(null, null);
|
this(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReactTextInlineImageViewManager(
|
public FrescoBasedReactTextInlineImageViewManager(
|
||||||
@Nullable AbstractDraweeControllerBuilder draweeControllerBuilder,
|
@Nullable AbstractDraweeControllerBuilder draweeControllerBuilder,
|
||||||
@Nullable Object callerContext) {
|
@Nullable Object callerContext) {
|
||||||
mDraweeControllerBuilder = draweeControllerBuilder;
|
mDraweeControllerBuilder = draweeControllerBuilder;
|
||||||
mCallerContext = callerContext;
|
mCallerContext = callerContext;
|
||||||
}
|
}
|
||||||
|
@ -52,18 +52,18 @@ public class ReactTextInlineImageViewManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReactTextInlineImageShadowNode createShadowNodeInstance() {
|
public FrescoBasedReactTextInlineImageShadowNode createShadowNodeInstance() {
|
||||||
return new ReactTextInlineImageShadowNode(
|
return new FrescoBasedReactTextInlineImageShadowNode(
|
||||||
(mDraweeControllerBuilder != null) ?
|
(mDraweeControllerBuilder != null) ?
|
||||||
mDraweeControllerBuilder :
|
mDraweeControllerBuilder :
|
||||||
Fresco.newDraweeControllerBuilder(),
|
Fresco.newDraweeControllerBuilder(),
|
||||||
mCallerContext
|
mCallerContext
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<ReactTextInlineImageShadowNode> getShadowNodeClass() {
|
public Class<FrescoBasedReactTextInlineImageShadowNode> getShadowNodeClass() {
|
||||||
return ReactTextInlineImageShadowNode.class;
|
return FrescoBasedReactTextInlineImageShadowNode.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
|
@ -43,8 +43,8 @@ import com.facebook.react.uimanager.ViewProps;
|
||||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||||
import com.facebook.react.views.text.DefaultStyleValuesUtil;
|
import com.facebook.react.views.text.DefaultStyleValuesUtil;
|
||||||
import com.facebook.react.views.text.ReactTextUpdate;
|
|
||||||
import com.facebook.react.views.text.TextInlineImageSpan;
|
import com.facebook.react.views.text.TextInlineImageSpan;
|
||||||
|
import com.facebook.react.views.text.ReactTextUpdate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages instances of TextInput.
|
* Manages instances of TextInput.
|
||||||
|
|
Loading…
Reference in New Issue