Implement RCTTextInlineImage with Nodes

Summary:
React allows nesting <Image> inside <Text>, in which case it turns into an RCTTextInlineImage element. RNFeed is using it in a few places and thus we need to support it, too.

This diff implements it with InlineImageSpan (WithPipeline, and WithDrawee separately).

Reviewed By: ahmedre

Differential Revision: D2792569
This commit is contained in:
Denis Koroskin 2016-01-04 16:01:01 -08:00 committed by Ahmed El-Helw
parent 28efab0670
commit 31d2443dd4
8 changed files with 289 additions and 1 deletions

View File

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

View File

@ -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);

View File

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

View File

@ -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;

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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<RCTTextInlineImage> {
@Override
public String getName() {
return "RCTTextInlineImage";
}
@Override
public RCTTextInlineImage createShadowNodeInstance() {
return new RCTTextInlineImage();
}
@Override
public Class<RCTTextInlineImage> getShadowNodeClass() {
return RCTTextInlineImage.class;
}
}

View File

@ -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;