From 9baef484984d161c77a60ae3fcd5aebf6fd25c08 Mon Sep 17 00:00:00 2001 From: Felix Oghina Date: Fri, 26 Feb 2016 06:15:58 -0800 Subject: [PATCH] decouple textview from fresco Reviewed By: andreicoman11 Differential Revision: D2960626 fb-gh-sync-id: c03aa7f16cdea795cefe39da2c5d660ae6278a37 shipit-source-id: c03aa7f16cdea795cefe39da2c5d660ae6278a37 --- .../main/java/com/facebook/react/shell/BUCK | 1 + .../react/shell/MainReactPackage.java | 8 +- .../java/com/facebook/react/views/text/BUCK | 4 - .../react/views/text/ReactRawTextManager.java | 1 - .../text/ReactTextInlineImageShadowNode.java | 87 +------- .../react/views/text/ReactTextShadowNode.java | 54 ++--- .../react/views/text/TextInlineImageSpan.java | 201 +++++------------- .../react/views/text/frescosupport/BUCK | 27 +++ ...coBasedReactTextInlineImageShadowNode.java | 114 ++++++++++ .../FrescoBasedReactTextInlineImageSpan.java | 155 ++++++++++++++ ...BasedReactTextInlineImageViewManager.java} | 34 +-- .../textinput/ReactTextInputManager.java | 2 +- 12 files changed, 396 insertions(+), 292 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageSpan.java rename ReactAndroid/src/main/java/com/facebook/react/views/text/{ReactTextInlineImageViewManager.java => frescosupport/FrescoBasedReactTextInlineImageViewManager.java} (56%) diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index 88b94ebda..ec0c816b8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -19,6 +19,7 @@ android_library( 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/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/toolbar:toolbar'), react_native_target('java/com/facebook/react/views/view:view'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index d3ec2880f..01a30148d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -22,8 +22,8 @@ import com.facebook.react.modules.camera.CameraRollManager; import com.facebook.react.modules.camera.ImageEditingManager; import com.facebook.react.modules.camera.ImageStoreManager; 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.dialog.DialogModule; import com.facebook.react.modules.fresco.FrescoModule; import com.facebook.react.modules.intent.IntentModule; 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.scroll.ReactHorizontalScrollViewManager; 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.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.textfrescosupport.FrescoBasedReactTextInlineImageViewManager; import com.facebook.react.views.textinput.ReactTextInputManager; import com.facebook.react.views.toolbar.ReactToolbarManager; import com.facebook.react.views.view.ReactViewManager; import com.facebook.react.views.viewpager.ReactViewPagerManager; -import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager; import com.facebook.react.views.webview.ReactWebViewManager; /** @@ -106,7 +106,7 @@ public class MainReactPackage implements ReactPackage { new ReactRawTextManager(), new ReactScrollViewManager(), new ReactSwitchManager(), - new ReactTextInlineImageViewManager(), + new FrescoBasedReactTextInlineImageViewManager(), new ReactTextInputManager(), new ReactTextViewManager(), new ReactToolbarManager(), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK index df5bd7dee..8d1e02e6c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK @@ -9,10 +9,6 @@ android_library( 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/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/jsr-305:jsr-305'), ], diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java index aa71c74b9..98e02829a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java @@ -10,7 +10,6 @@ package com.facebook.react.views.text; import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ThemedReactContext; /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextInlineImageShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextInlineImageShadowNode.java index 3a4eed131..4e986644e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextInlineImageShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextInlineImageShadowNode.java @@ -9,90 +9,17 @@ 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.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; - private final @Nullable Object mCallerContext; - - public ReactTextInlineImageShadowNode( - 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; - } + /** + * Build a {@link TextInlineImageSpan} from this node. This will be added to the TextView in + * place of this node. + */ + public abstract TextInlineImageSpan buildInlineImageSpan(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index 314df23ef..2d98703c2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -14,7 +14,6 @@ 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; @@ -37,11 +36,11 @@ import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.ReactShadowNode; import com.facebook.react.uimanager.UIViewOperationQueue; import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; /** * {@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, SpannableStringBuilder sb, List ops) { @@ -107,7 +106,14 @@ public class ReactTextShadowNode extends LayoutShadowNode { if (child instanceof ReactTextShadowNode) { buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops); } 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 { throw new IllegalViewOperationException("Unexpected view type nested under text node: " + child.getClass()); @@ -154,36 +160,14 @@ public class ReactTextShadowNode extends LayoutShadowNode { } } - private static final void buildSpannedFromImageNode( - ReactTextInlineImageShadowNode node, - SpannableStringBuilder sb, - List 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) { + protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) { SpannableStringBuilder sb = new SpannableStringBuilder(); // 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 // up-to-bottom, otherwise all the spannables that are withing the region for which one may set // a new spannable will be wiped out - List ops = new ArrayList(); + List ops = new ArrayList<>(); buildSpannedFromTextCSSNode(textCSSNode, sb, ops); if (textCSSNode.mFontSize == UNSET) { sb.setSpan( @@ -330,6 +314,13 @@ public class ReactTextShadowNode extends LayoutShadowNode { protected boolean mContainsImages = false; + public ReactTextShadowNode(boolean isVirtual) { + mIsVirtual = isVirtual; + if (!isVirtual) { + setMeasureFunction(TEXT_MEASURE_FUNCTION); + } + } + @Override public void onBeforeLayout() { if (mIsVirtual) { @@ -483,11 +474,4 @@ public class ReactTextShadowNode extends LayoutShadowNode { uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); } } - - public ReactTextShadowNode(boolean isVirtual) { - mIsVirtual = isVirtual; - if (!isVirtual) { - setMeasureFunction(TEXT_MEASURE_FUNCTION); - } - } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextInlineImageSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextInlineImageSpan.java index 6079ce00b..0308ee20c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextInlineImageSpan.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextInlineImageSpan.java @@ -7,163 +7,64 @@ * 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.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 android.graphics.drawable.Drawable; + import android.text.Spannable; + import android.text.style.ReplacementSpan; + import android.view.View; + import android.widget.TextView; -import com.facebook.drawee.backends.pipeline.Fresco; -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; + /** + * Base class for inline image spans. + */ + public abstract class TextInlineImageSpan extends ReplacementSpan { -/** - * TextInlineImageSpan is a span for Images that are inside . 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 { + /** + * 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.setTextView(view); + } + } - private @Nullable Drawable mDrawable; - private final AbstractDraweeControllerBuilder mDraweeControllerBuilder; - private final DraweeHolder mDraweeHolder; - private final @Nullable Object mCallerContext; + /** + * Get the drawable that is span represents. + */ + public abstract @Nullable Drawable getDrawable(); - private int mHeight; - private Uri mUri; - private int mWidth; + /** + * Called by the text view from {@link View#onDetachedFromWindow()}, + */ + 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, - int height, - int width, - @Nullable Uri uri, - AbstractDraweeControllerBuilder draweeControllerBuilder, - @Nullable Object callerContext) { - mDraweeHolder = new DraweeHolder( - GenericDraweeHierarchyBuilder.newInstance(resources) - .build() - ); - mDraweeControllerBuilder = draweeControllerBuilder; - mCallerContext = callerContext; + /** + * Called by the text view from {@link View#onAttachedToWindow()}. + */ + public abstract void onAttachedToWindow(); - mHeight = height; - mWidth = width; - mUri = (uri != null) ? uri : Uri.EMPTY; - } + /** + * Called by the text view from {@link View#onFinishTemporaryDetach()}. + */ + public abstract void onFinishTemporaryDetach(); - /** - * 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 = 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; - } - }; -} + /** + * Set the textview that will contain this span. + */ + public abstract void setTextView(TextView textView); + } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/BUCK new file mode 100644 index 000000000..e8ddbceb6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/BUCK @@ -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', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java new file mode 100644 index 000000000..643a2c136 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java @@ -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; + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageSpan.java new file mode 100644 index 000000000..d62ecf0ce --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageSpan.java @@ -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 . 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 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(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextInlineImageViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageViewManager.java similarity index 56% rename from ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextInlineImageViewManager.java rename to ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageViewManager.java index b16b42dea..c272bf39d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextInlineImageViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageViewManager.java @@ -7,7 +7,7 @@ * 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; @@ -19,24 +19,24 @@ 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} + * Manages Images embedded in Text nodes using Fresco. 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 { +public class FrescoBasedReactTextInlineImageViewManager + extends ViewManager { static final String REACT_CLASS = "RCTTextInlineImage"; private final @Nullable AbstractDraweeControllerBuilder mDraweeControllerBuilder; private final @Nullable Object mCallerContext; - public ReactTextInlineImageViewManager() { + public FrescoBasedReactTextInlineImageViewManager() { this(null, null); } - public ReactTextInlineImageViewManager( - @Nullable AbstractDraweeControllerBuilder draweeControllerBuilder, - @Nullable Object callerContext) { + public FrescoBasedReactTextInlineImageViewManager( + @Nullable AbstractDraweeControllerBuilder draweeControllerBuilder, + @Nullable Object callerContext) { mDraweeControllerBuilder = draweeControllerBuilder; mCallerContext = callerContext; } @@ -52,18 +52,18 @@ public class ReactTextInlineImageViewManager } @Override - public ReactTextInlineImageShadowNode createShadowNodeInstance() { - return new ReactTextInlineImageShadowNode( - (mDraweeControllerBuilder != null) ? - mDraweeControllerBuilder : - Fresco.newDraweeControllerBuilder(), - mCallerContext + public FrescoBasedReactTextInlineImageShadowNode createShadowNodeInstance() { + return new FrescoBasedReactTextInlineImageShadowNode( + (mDraweeControllerBuilder != null) ? + mDraweeControllerBuilder : + Fresco.newDraweeControllerBuilder(), + mCallerContext ); } @Override - public Class getShadowNodeClass() { - return ReactTextInlineImageShadowNode.class; + public Class getShadowNodeClass() { + return FrescoBasedReactTextInlineImageShadowNode.class; } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index d6ce4f0de..2ac07ddc6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -43,8 +43,8 @@ import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.annotations.ReactProp; 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; +import com.facebook.react.views.text.ReactTextUpdate; /** * Manages instances of TextInput.