diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java index 0effe2b84..6bea964ae 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java @@ -59,4 +59,5 @@ import com.facebook.react.uimanager.ReactShadowNode; protected abstract void performCollectText(SpannableStringBuilder builder); protected abstract void performApplySpans(SpannableStringBuilder builder, int begin, int end); + protected abstract void performCollectAttachDetachListeners(StateBuilder stateBuilder); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIImplementation.java index 0cea29964..17adb2d93 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIImplementation.java @@ -67,6 +67,7 @@ public class FlatUIImplementation extends UIImplementation { viewManagers.add(new RCTTextManager()); viewManagers.add(new RCTRawTextManager()); viewManagers.add(new RCTVirtualTextManager()); + viewManagers.add(new RCTTextInlineImageManager()); viewManagers.add(new RCTImageViewManager()); ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers); diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/InlineImageSpanWithPipeline.java b/ReactAndroid/src/main/java/com/facebook/react/flat/InlineImageSpanWithPipeline.java new file mode 100644 index 000000000..b0d197690 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/InlineImageSpanWithPipeline.java @@ -0,0 +1,161 @@ +/** + * 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.flat; + +import javax.annotation.Nullable; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.text.style.ReplacementSpan; + +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.infer.annotation.Assertions; + +/* package */ final class InlineImageSpanWithPipeline extends ReplacementSpan + implements AttachDetachListener, BitmapUpdateListener { + + private static final RectF TMP_RECT = new RectF(); + + private @Nullable PipelineRequestHelper mRequestHelper; + private @Nullable FlatViewGroup.InvalidateCallback mCallback; + private float mWidth; + private float mHeight; + private boolean mFrozen; + + /* package */ InlineImageSpanWithPipeline() { + this(null, Float.NaN, Float.NaN); + } + + private InlineImageSpanWithPipeline( + @Nullable PipelineRequestHelper requestHelper, + float width, + float height) { + mRequestHelper = requestHelper; + mWidth = width; + mHeight = height; + } + + /* package */ InlineImageSpanWithPipeline mutableCopy() { + return new InlineImageSpanWithPipeline(mRequestHelper, mWidth, mHeight); + } + + /* package */ boolean hasImageRequest() { + return mRequestHelper != null; + } + + /** + * Assigns a new image request to the DrawImage, or null to clear the image request. + */ + /* package */ void setImageRequest(@Nullable ImageRequest imageRequest) { + if (imageRequest == null) { + mRequestHelper = null; + } else { + mRequestHelper = new PipelineRequestHelper(imageRequest); + } + } + + /* package */ float getWidth() { + return mWidth; + } + + /* package */ void setWidth(float width) { + mWidth = width; + } + + /* package */ float getHeight() { + return mHeight; + } + + /* package */ void setHeight(float height) { + mHeight = height; + } + + /* package */ void freeze() { + mFrozen = true; + } + + /* package */ boolean isFrozen() { + return mFrozen; + } + + @Override + public void onSecondaryAttach(Bitmap bitmap) { + // We don't know if width or height changed, so invalidate just in case. + Assertions.assumeNotNull(mCallback).invalidate(); + } + + @Override + public void onBitmapReady(Bitmap bitmap) { + // Bitmap is now ready, draw it. + Assertions.assumeNotNull(mCallback).invalidate(); + } + + @Override + public void onAttached(FlatViewGroup.InvalidateCallback callback) { + mCallback = callback; + + if (mRequestHelper != null) { + mRequestHelper.attach(this); + } + } + + @Override + public void onDetached() { + if (mRequestHelper != null) { + mRequestHelper.detach(); + + if (mRequestHelper.isDetached()) { + // optional + mCallback = null; + } + } + } + + @Override + public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { + if (fm != null) { + fm.ascent = -Math.round(mHeight); + fm.descent = 0; + + fm.top = fm.ascent; + fm.bottom = 0; + } + + return Math.round(mWidth); + } + + @Override + public void draw( + Canvas canvas, + CharSequence text, + int start, + int end, + float x, + int top, + int y, + int bottom, + Paint paint) { + if (mRequestHelper == null) { + return; + } + + Bitmap bitmap = mRequestHelper.getBitmap(); + if (bitmap == null) { + return; + } + + float bottomFloat = (float) bottom; + TMP_RECT.set(x, bottomFloat - mHeight, x + mWidth, bottomFloat); + + canvas.drawBitmap(bitmap, null, TMP_RECT, paint); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java index 880cc65ec..786c75eaa 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java @@ -19,7 +19,7 @@ import com.facebook.react.uimanager.ReactProp; /** * RCTRawText is a FlatTextShadowNode that can only contain raw text (but not styling). */ -/* package */ class RCTRawText extends FlatTextShadowNode { +/* package */ final class RCTRawText extends FlatTextShadowNode { private @Nullable String mText; @@ -39,6 +39,11 @@ import com.facebook.react.uimanager.ReactProp; Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } + @Override + protected void performCollectAttachDetachListeners(StateBuilder stateBuilder) { + // nothing to do + } + @ReactProp(name = "text") public void setText(@Nullable String text) { mText = text; diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java index b897b9cd8..1bef0088d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java @@ -201,6 +201,8 @@ import com.facebook.react.uimanager.ViewProps; clipRight, clipBottom); stateBuilder.addDrawCommand(mDrawCommand); + + performCollectAttachDetachListeners(stateBuilder); } @ReactProp(name = ViewProps.LINE_HEIGHT, defaultDouble = Double.NaN) diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInlineImage.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInlineImage.java new file mode 100644 index 000000000..ade633a50 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInlineImage.java @@ -0,0 +1,79 @@ +/** + * 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.flat; + +import javax.annotation.Nullable; + +import android.text.SpannableStringBuilder; +import android.text.Spanned; + +import com.facebook.react.uimanager.ReactProp; + +/** + * RCTTextInlineImage + */ +/* package */ class RCTTextInlineImage extends FlatTextShadowNode { + + private InlineImageSpanWithPipeline mInlineImageSpan = new InlineImageSpanWithPipeline(); + + @Override + public void setStyleWidth(float width) { + super.setStyleWidth(width); + + if (mInlineImageSpan.getWidth() != width) { + getMutableSpan().setWidth(width); + notifyChanged(true); + } + } + + @Override + public void setStyleHeight(float height) { + super.setStyleHeight(height); + + if (mInlineImageSpan.getHeight() != height) { + getMutableSpan().setHeight(height); + notifyChanged(true); + } + } + + @Override + protected void performCollectText(SpannableStringBuilder builder) { + builder.append("I"); + } + + @Override + protected void performApplySpans(SpannableStringBuilder builder, int begin, int end) { + mInlineImageSpan.freeze(); + builder.setSpan( + mInlineImageSpan, + begin, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Override + protected void performCollectAttachDetachListeners(StateBuilder stateBuilder) { + // mInlineImageSpan should already be frozen so no need to freeze it again + stateBuilder.addAttachDetachListener(mInlineImageSpan); + } + + @ReactProp(name = "src") + public void setSource(@Nullable String source) { + getMutableSpan().setImageRequest( + ImageRequestHelper.createImageRequest(getThemedContext(), source)); + } + + private InlineImageSpanWithPipeline getMutableSpan() { + if (mInlineImageSpan.isFrozen()) { + mInlineImageSpan = mInlineImageSpan.mutableCopy(); + } + return mInlineImageSpan; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInlineImageManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInlineImageManager.java new file mode 100644 index 000000000..5f422cefb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInlineImageManager.java @@ -0,0 +1,31 @@ +/** + * 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.flat; + +/** + * ViewManager that creates instances of RCTTextInlineImage. + */ +/* package */ final class RCTTextInlineImageManager extends VirtualViewManager { + + @Override + public String getName() { + return "RCTTextInlineImage"; + } + + @Override + public RCTTextInlineImage createShadowNodeInstance() { + return new RCTTextInlineImage(); + } + + @Override + public Class getShadowNodeClass() { + return RCTTextInlineImage.class; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java index 167d5a0d9..8f1179277 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java @@ -59,6 +59,14 @@ import com.facebook.react.uimanager.ViewProps; } } + @Override + protected void performCollectAttachDetachListeners(StateBuilder stateBuilder) { + for (int i = 0, childCount = getChildCount(); i < childCount; ++i) { + FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i); + child.performCollectAttachDetachListeners(stateBuilder); + } + } + @ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = Float.NaN) public void setFontSize(float fontSizeSp) { final int fontSize;