Android: SVG Images not rendering the first time a view is displayed

Since SVG images are loaded asynchronously, we need to invalidate the
view after drawing to the canvas so that the view will be re-rendered.

Storing a reference to the actual SvgView in ViewManager, and passing a
reference to the ViewManager to each shadow node, so that shadow nodes
can invalidate the root SVG view.
This commit is contained in:
Greg Hogan
2016-07-29 13:04:03 -07:00
parent ab064d86e0
commit cf6ec2c022
4 changed files with 64 additions and 27 deletions
@@ -16,12 +16,10 @@ import android.graphics.Paint;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import com.facebook.common.executors.UiThreadImmediateExecutorService; import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.logging.FLog; import com.facebook.common.logging.FLog;
import com.facebook.common.references.CloseableReference; import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource; import com.facebook.datasource.DataSource;
import com.facebook.common.logging.FLog;
import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableBitmap; import com.facebook.imagepipeline.image.CloseableBitmap;
@@ -31,6 +29,9 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants; import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -44,7 +45,7 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
private String mW; private String mW;
private String mH; private String mH;
private Uri mUri; private Uri mUri;
private boolean mLoading; private AtomicBoolean mLoading = new AtomicBoolean(false);
@ReactProp(name = "x") @ReactProp(name = "x")
public void setX(String x) { public void setX(String x) {
@@ -86,13 +87,14 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
@Override @Override
public void draw(final Canvas canvas, final Paint paint, final float opacity) { public void draw(final Canvas canvas, final Paint paint, final float opacity) {
final ImageRequest request = ImageRequestBuilder.newBuilderWithSource(mUri).build(); if (!mLoading.get()) {
final boolean inMemoryCache = Fresco.getImagePipeline().isInBitmapMemoryCache(request); final ImageRequest request = ImageRequestBuilder.newBuilderWithSource(mUri).build();
if (inMemoryCache) { if (Fresco.getImagePipeline().isInBitmapMemoryCache(request)) {
tryRender(request, canvas, paint, opacity * mOpacity); tryRender(request, canvas, paint, opacity * mOpacity);
} else if (!mLoading) { } else {
loadBitmap(request, canvas, paint); loadBitmap(request, canvas, paint);
}
} }
} }
@@ -106,8 +108,10 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
if (bitmap != null) { if (bitmap != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
paint.reset(); paint.reset();
mLoading = false; mLoading.set(false);
getSvgShadowNode().drawChildren(canvas, paint); getSvgShadowNode().drawChildren(canvas, paint);
getSvgShadowNode().invalidateView(getRect());
} }
} }
@@ -115,7 +119,7 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
public void onFailureImpl(DataSource dataSource) { public void onFailureImpl(DataSource dataSource) {
// No cleanup required here. // No cleanup required here.
// TODO: more details about this failure // TODO: more details about this failure
mLoading = false; mLoading.set(false);
FLog.w(ReactConstants.TAG, dataSource.getFailureCause(), "RNSVG: fetchDecodedImage failed!"); FLog.w(ReactConstants.TAG, dataSource.getFailureCause(), "RNSVG: fetchDecodedImage failed!");
} }
}, },
@@ -123,18 +127,25 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
); );
} }
private void doRender(@Nonnull final Canvas canvas, @Nonnull final Paint paint, @Nonnull final Bitmap bitmap, final float opacity) { @Nonnull
final int count = saveAndSetupCanvas(canvas); private Rect getRect() {
clip(canvas, paint);
float x = PropHelper.fromPercentageToFloat(mX, mCanvasWidth, 0, mScale); float x = PropHelper.fromPercentageToFloat(mX, mCanvasWidth, 0, mScale);
float y = PropHelper.fromPercentageToFloat(mY, mCanvasHeight, 0, mScale); float y = PropHelper.fromPercentageToFloat(mY, mCanvasHeight, 0, mScale);
float w = PropHelper.fromPercentageToFloat(mW, mCanvasWidth, 0, mScale); float w = PropHelper.fromPercentageToFloat(mW, mCanvasWidth, 0, mScale);
float h = PropHelper.fromPercentageToFloat(mH, mCanvasHeight, 0, mScale); float h = PropHelper.fromPercentageToFloat(mH, mCanvasHeight, 0, mScale);
return new Rect((int) x, (int) y, (int) (x + w), (int) (y + h));
}
private void doRender(@Nonnull final Canvas canvas, @Nonnull final Paint paint, @Nonnull final Bitmap bitmap, final float opacity) {
final int count = saveAndSetupCanvas(canvas);
clip(canvas, paint);
Paint alphaPaint = new Paint(); Paint alphaPaint = new Paint();
alphaPaint.setAlpha((int) (opacity * 255)); alphaPaint.setAlpha((int) (opacity * 255));
canvas.drawBitmap(bitmap, null, new Rect((int) x, (int) y, (int) (x + w), (int) (y + h)), alphaPaint); canvas.drawBitmap(bitmap, null, getRect(), alphaPaint);
restoreCanvas(canvas, count); restoreCanvas(canvas, count);
markUpdateSeen(); markUpdateSeen();
@@ -10,13 +10,14 @@
package com.horcrux.svg; package com.horcrux.svg;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.util.Log;
import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ViewGroupManager;
import java.util.ArrayList; import java.util.ArrayList;
import javax.annotation.Nullable;
/** /**
* ViewManager for RNSVGSvgView React views. Renders as a {@link RNSVGSvgView} and handles * ViewManager for RNSVGSvgView React views. Renders as a {@link RNSVGSvgView} and handles
* invalidating the native view on shadow view updates happening in the underlying tree. * invalidating the native view on shadow view updates happening in the underlying tree.
@@ -24,6 +25,7 @@ import java.util.ArrayList;
public class RNSVGSvgViewManager extends ViewGroupManager<RNSVGSvgView> { public class RNSVGSvgViewManager extends ViewGroupManager<RNSVGSvgView> {
private static final String REACT_CLASS = "RNSVGSvgView"; private static final String REACT_CLASS = "RNSVGSvgView";
private RNSVGSvgView svgView = null;
// TODO: use an ArrayList to connect RNSVGSvgViewShadowNode with RNSVGSvgView, not sure if there will be a race condition. // TODO: use an ArrayList to connect RNSVGSvgViewShadowNode with RNSVGSvgView, not sure if there will be a race condition.
// TODO: find a better way to replace this // TODO: find a better way to replace this
@@ -34,9 +36,14 @@ public class RNSVGSvgViewManager extends ViewGroupManager<RNSVGSvgView> {
return REACT_CLASS; return REACT_CLASS;
} }
@Nullable
public RNSVGSvgView getSvgView() {
return this.svgView;
}
@Override @Override
public RNSVGSvgViewShadowNode createShadowNodeInstance() { public RNSVGSvgViewShadowNode createShadowNodeInstance() {
RNSVGSvgViewShadowNode node = new RNSVGSvgViewShadowNode(); RNSVGSvgViewShadowNode node = new RNSVGSvgViewShadowNode(this);
SvgShadowNodes.add(node); SvgShadowNodes.add(node);
return node; return node;
} }
@@ -50,7 +57,8 @@ public class RNSVGSvgViewManager extends ViewGroupManager<RNSVGSvgView> {
protected RNSVGSvgView createViewInstance(ThemedReactContext reactContext) { protected RNSVGSvgView createViewInstance(ThemedReactContext reactContext) {
RNSVGSvgViewShadowNode shadowNode = SvgShadowNodes.get(0); RNSVGSvgViewShadowNode shadowNode = SvgShadowNodes.get(0);
SvgShadowNodes.remove(0); SvgShadowNodes.remove(0);
return new RNSVGSvgView(reactContext, shadowNode); this.svgView = new RNSVGSvgView(reactContext, shadowNode);
return this.svgView;
} }
@Override @Override
@@ -13,15 +13,19 @@ import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Point; import android.graphics.Point;
import android.util.Log; import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.UIViewOperationQueue; import com.facebook.react.uimanager.UIViewOperationQueue;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.annotation.Nonnull;
/** /**
* Shadow node for RNSVG virtual tree root - RNSVGSvgView * Shadow node for RNSVG virtual tree root - RNSVGSvgView
*/ */
@@ -32,6 +36,13 @@ public class RNSVGSvgViewShadowNode extends LayoutShadowNode {
private static final Map<String, RNSVGVirtualNode> mDefinedTemplates = new HashMap<>(); private static final Map<String, RNSVGVirtualNode> mDefinedTemplates = new HashMap<>();
private static final Map<String, PropHelper.RNSVGBrush> mDefinedBrushes = new HashMap<>(); private static final Map<String, PropHelper.RNSVGBrush> mDefinedBrushes = new HashMap<>();
@Nonnull private final RNSVGSvgViewManager viewManager;
public RNSVGSvgViewShadowNode(@Nonnull final RNSVGSvgViewManager viewManager) {
super();
this.viewManager = viewManager;
}
@Override @Override
public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) { public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) {
super.onCollectExtraUpdates(uiUpdater); super.onCollectExtraUpdates(uiUpdater);
@@ -54,7 +65,7 @@ public class RNSVGSvgViewShadowNode extends LayoutShadowNode {
* Draw all of the child nodes of this root node * Draw all of the child nodes of this root node
* *
* This method is synchronized since * This method is synchronized since
* {@link com.horcrux.svg.RNSVGImageShadowNode#loadImage(ImageRequest, Canvas, Paint)} calls it * {@link com.horcrux.svg.RNSVGImageShadowNode#loadBitmap(ImageRequest, Canvas, Paint)} calls it
* asynchronously after images have loaded and are ready to be drawn. * asynchronously after images have loaded and are ready to be drawn.
* *
* @param canvas * @param canvas
@@ -77,6 +88,16 @@ public class RNSVGSvgViewShadowNode extends LayoutShadowNode {
} }
} }
protected void invalidateView(@Nonnull final Rect dirtyRect) {
final RNSVGSvgView svgView = this.viewManager.getSvgView();
if (svgView != null) {
final View rootView = svgView.getRootView();
if (rootView != null) {
rootView.invalidate(dirtyRect);
}
}
}
public void enableTouchEvents() { public void enableTouchEvents() {
if (!mResponsible) { if (!mResponsible) {
mResponsible = true; mResponsible = true;
@@ -9,8 +9,6 @@
package com.horcrux.svg; package com.horcrux.svg;
import javax.annotation.Nullable;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
@@ -18,16 +16,16 @@ import android.graphics.Path;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Region; import android.graphics.Region;
import android.util.Log;
import android.view.View; import android.view.View;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.DisplayMetricsHolder;
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; import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.annotations.ReactProp;
import javax.annotation.Nullable;
/** /**
* Base class for RNSVGView virtual nodes: {@link RNSVGGroupShadowNode}, {@link RNSVGPathShadowNode} and * Base class for RNSVGView virtual nodes: {@link RNSVGGroupShadowNode}, {@link RNSVGPathShadowNode} and
@@ -81,8 +79,7 @@ public abstract class RNSVGVirtualNode extends LayoutShadowNode {
* @param canvas the canvas to set up * @param canvas the canvas to set up
*/ */
protected final int saveAndSetupCanvas(Canvas canvas) { protected final int saveAndSetupCanvas(Canvas canvas) {
int count = canvas.getSaveCount(); final int count = canvas.save();
canvas.save();
if (mMatrix != null) { if (mMatrix != null) {
canvas.concat(mMatrix); canvas.concat(mMatrix);
} }