From 760422525e24d53ceead6a03b82713d2df0e8813 Mon Sep 17 00:00:00 2001 From: Denis Koroskin Date: Fri, 11 Dec 2015 22:44:38 -0800 Subject: [PATCH] Add support for RCTImageView in FlatShadowHierarchyManager Summary: @public This patch adds basic support for RCTImageView (only 'src', 'tintColor' and 'resizeMode' properties are supported for now), and a concept of AttachDetachListener that is required to support it to FlatUIImplementations. Reviewed By: sriramramani Differential Revision: D2564389 --- .../react/flat/AbstractDrawCommand.java | 14 ++ .../react/flat/AttachDetachListener.java | 30 +++ .../react/flat/BitmapRequestHelper.java | 171 ++++++++++++++++++ .../com/facebook/react/flat/DrawImage.java | 46 +++++ .../react/flat/DrawImageWithPipeline.java | 144 +++++++++++++++ .../flat/FlatNativeViewHierarchyManager.java | 19 +- .../react/flat/FlatRootShadowNode.java | 41 +++++ .../facebook/react/flat/FlatShadowNode.java | 11 ++ .../react/flat/FlatUIImplementation.java | 36 +++- .../react/flat/FlatUIViewOperationQueue.java | 23 ++- .../facebook/react/flat/FlatViewGroup.java | 96 ++++++++++ .../com/facebook/react/flat/RCTImageView.java | 117 ++++++++++++ .../react/flat/RCTImageViewManager.java | 28 +++ .../com/facebook/react/flat/StateBuilder.java | 22 ++- 14 files changed, 784 insertions(+), 14 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/AttachDetachListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageViewManager.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java index 37df9f2d8..007145edd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java @@ -54,6 +54,20 @@ package com.facebook.react.flat; return this; } + /** + * Returns a non-frozen shallow copy of AbstractDrawCommand as defined by {@link Object#clone()}. + */ + public final AbstractDrawCommand mutableCopy() { + try { + AbstractDrawCommand copy = (AbstractDrawCommand) super.clone(); + copy.mFrozen = false; + return copy; + } catch (CloneNotSupportedException e) { + // should not happen since we implement Cloneable + throw new RuntimeException(e); + } + } + /** * Returns whether this object was frozen and thus cannot be mutated. */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/AttachDetachListener.java b/ReactAndroid/src/main/java/com/facebook/react/flat/AttachDetachListener.java new file mode 100644 index 000000000..af51827b9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/AttachDetachListener.java @@ -0,0 +1,30 @@ +/** + * 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; + +/** + * An interface that DrawCommands need to implement into order to receive + * {@link android.view.View#onAttachedToWindow()} and + * {@link android.view.View#onDetachedFromWindow()} events. + */ +/* package */ interface AttachDetachListener { + public static final AttachDetachListener[] EMPTY_ARRAY = new AttachDetachListener[0]; + + /** + * Called when a DrawCommand is being attached to a visible View hierarchy. + * @param callback a WeakReference to a View that provides invalidate() helper method. + */ + public void onAttached(FlatViewGroup.InvalidateCallback callback); + + /** + * Called when a DrawCommand is being detached from a visible View hierarchy. + */ + public void onDetached(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java b/ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java new file mode 100644 index 000000000..193b1535d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java @@ -0,0 +1,171 @@ +/** + * 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 com.facebook.common.executors.UiThreadImmediateExecutorService; +import com.facebook.common.references.CloseableReference; +import com.facebook.datasource.DataSource; +import com.facebook.datasource.DataSubscriber; +import com.facebook.imagepipeline.core.ImagePipeline; +import com.facebook.imagepipeline.core.ImagePipelineFactory; +import com.facebook.imagepipeline.image.CloseableBitmap; +import com.facebook.imagepipeline.image.CloseableImage; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.infer.annotation.Assertions; + +/** + * Helper class for DrawImage that helps manage fetch requests through ImagePipeline. + * + * Request states this class can be in: + * 1) mDataSource == null, mImageRef == null : request has not be started, was canceled or failed. + * 2) mDataSource != null, mImageRef == null : request is in progress. + * 3) mDataSource == null, mImageRef != null : request successfully finished. + * 4) mDataSource != null, mImageRef != null : invalid state (should never happen) + */ +/* package */ final class BitmapRequestHelper + implements DataSubscriber> { + + private final ImageRequest mImageRequest; + private final DrawImageWithPipeline mDrawImage; + private @Nullable DataSource> mDataSource; + private @Nullable CloseableReference mImageRef; + private int mAttachCounter; + + /* package */ BitmapRequestHelper(ImageRequest imageRequest, DrawImageWithPipeline drawImage) { + mImageRequest = imageRequest; + mDrawImage = drawImage; + } + + /* package */ void attach() { + mAttachCounter++; + if (mAttachCounter != 1) { + // this is a secondary attach, ignore it, only updating Bitmap boundaries if needed. + Bitmap bitmap = getBitmap(); + if (bitmap != null) { + mDrawImage.updateBounds(bitmap); + } + return; + } + + Assertions.assertCondition(mDataSource == null); + Assertions.assertCondition(mImageRef == null); + + // Submit the request + ImagePipeline imagePipeline = ImagePipelineFactory.getInstance().getImagePipeline(); + mDataSource = imagePipeline.fetchDecodedImage(mImageRequest, RCTImageView.getCallerContext()); + mDataSource.subscribe(this, UiThreadImmediateExecutorService.getInstance()); + } + + /** + * Returns whether detach() was primary, false otherwise. + */ + /* package */ void detach() { + --mAttachCounter; + if (mAttachCounter != 0) { + // this is a secondary detach, ignore it + return; + } + + if (mDataSource != null) { + mDataSource.close(); + mDataSource = null; + } + + if (mImageRef != null) { + mImageRef.close(); + mImageRef = null; + } + } + + /** + * Returns an unsafe bitmap reference. Do not assign the result of this method to anything other + * than a local variable, or it will no longer work with the reference count goes to zero. + */ + /* package */ @Nullable Bitmap getBitmap() { + if (mImageRef == null) { + return null; + } + + CloseableImage closeableImage = mImageRef.get(); + if (!(closeableImage instanceof CloseableBitmap)) { + mImageRef.close(); + mImageRef = null; + return null; + } + + return ((CloseableBitmap) closeableImage).getUnderlyingBitmap(); + } + + @Override + public void onNewResult(DataSource> dataSource) { + if (!dataSource.isFinished()) { + // only interested in final image, no need to close the dataSource + return; + } + + try { + if (mDataSource != dataSource) { + // Shouldn't ever happen, but let's be safe (dataSource got closed by callback still fired?) + return; + } + + mDataSource = null; + + CloseableReference imageReference = dataSource.getResult(); + if (imageReference == null) { + // Shouldn't ever happen, but let's be safe (dataSource got closed by callback still fired?) + return; + } + + CloseableImage image = imageReference.get(); + if (!(image instanceof CloseableBitmap)) { + // only bitmaps are supported + imageReference.close(); + return; + } + + mImageRef = imageReference; + + Bitmap bitmap = getBitmap(); + if (bitmap == null) { + // Shouldn't ever happen, but let's be safe. + return; + } + + // now that we have the Bitmap, DrawImage can finally initialize its + // tranformation matrix to satisfy requested ScaleType. + mDrawImage.updateBounds(bitmap); + } finally { + dataSource.close(); + } + } + + @Override + public void onFailure(DataSource> dataSource) { + if (mDataSource != dataSource) { + // Should always be the case, but let's be safe. + mDataSource = null; + } + + dataSource.close(); + } + + @Override + public void onCancellation(DataSource> dataSource) { + } + + @Override + public void onProgressUpdate(DataSource> dataSource) { + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java new file mode 100644 index 000000000..d8a7e7367 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java @@ -0,0 +1,46 @@ +/** + * 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 com.facebook.drawee.drawable.ScalingUtils.ScaleType; +import com.facebook.imagepipeline.request.ImageRequest; + +/** + * Common interface for DrawImageWithPipeline and DrawImageWithDrawee. + */ +/* package */ interface DrawImage extends DrawCommand, AttachDetachListener { + /** + * Returns true if an image source was assigned to the DrawImage. + * A DrawImage with no source will not draw anything. + */ + public boolean hasImageRequest(); + + /** + * Assigns a new image request to the DrawImage, or null to clear the image request. + */ + public void setImageRequest(@Nullable ImageRequest imageRequest); + + /** + * Assigns a tint color to apply to the image drawn. + */ + public void setTintColor(int tintColor); + + /** + * Assigns a scale type to draw to the image with. + */ + public void setScaleType(ScaleType scaleType); + + /** + * Returns a scale type to draw to the image with. + */ + public ScaleType getScaleType(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java new file mode 100644 index 000000000..f13a05a69 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java @@ -0,0 +1,144 @@ +/** + * 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.Matrix; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; + +import com.facebook.drawee.drawable.ScalingUtils.ScaleType; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.views.image.ImageResizeMode; + +/** + * DrawImageWithPipeline is DrawCommand that can draw a local or remote image. + * It uses BitmapRequestHelper internally to fetch and cache the images. + */ +/* package */ final class DrawImageWithPipeline extends AbstractDrawCommand implements DrawImage { + + private static final Paint PAINT = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + + private final Matrix mTransform = new Matrix(); + private ScaleType mScaleType = ImageResizeMode.defaultValue(); + private @Nullable BitmapRequestHelper mBitmapRequestHelper; + private @Nullable PorterDuffColorFilter mColorFilter; + private @Nullable FlatViewGroup.InvalidateCallback mCallback; + private boolean mForceClip; + + @Override + public boolean hasImageRequest() { + return mBitmapRequestHelper != null; + } + + @Override + public void setImageRequest(@Nullable ImageRequest imageRequest) { + if (imageRequest == null) { + mBitmapRequestHelper = null; + } else { + mBitmapRequestHelper = new BitmapRequestHelper(imageRequest, this); + } + } + + @Override + public void setTintColor(int tintColor) { + if (tintColor == 0) { + mColorFilter = null; + } else { + mColorFilter = new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP); + } + } + + @Override + public void setScaleType(ScaleType scaleType) { + mScaleType = scaleType; + } + + @Override + public ScaleType getScaleType() { + return mScaleType; + } + + @Override + public void draw(Canvas canvas) { + Bitmap bitmap = Assertions.assumeNotNull(mBitmapRequestHelper).getBitmap(); + if (bitmap == null) { + return; + } + + PAINT.setColorFilter(mColorFilter); + if (mForceClip) { + canvas.save(); + canvas.clipRect(getLeft(), getTop(), getRight(), getBottom()); + canvas.drawBitmap(bitmap, mTransform, PAINT); + canvas.restore(); + } else { + canvas.drawBitmap(bitmap, mTransform, PAINT); + } + } + + @Override + public void onAttached(FlatViewGroup.InvalidateCallback callback) { + mCallback = callback; + Assertions.assumeNotNull(mBitmapRequestHelper).attach(); + } + + @Override + public void onDetached() { + Assertions.assumeNotNull(mBitmapRequestHelper).detach(); + } + + /* package */ void updateBounds(Bitmap bitmap) { + Assertions.assumeNotNull(mCallback).invalidate(); + + float left = getLeft(); + float top = getTop(); + + float containerWidth = getRight() - left; + float containerHeight = getBottom() - top; + + float imageWidth = (float) bitmap.getWidth(); + float imageHeight = (float) bitmap.getHeight(); + + mForceClip = false; + + if (mScaleType == ScaleType.FIT_XY) { + mTransform.setScale(containerWidth / imageWidth, containerHeight / imageHeight); + mTransform.postTranslate(left, top); + return; + } + + final float scale; + + if (mScaleType == ScaleType.CENTER_INSIDE) { + final float ratio; + if (containerWidth >= imageWidth && containerHeight >= imageHeight) { + scale = 1.0f; + } else { + scale = Math.min(containerWidth / imageWidth, containerHeight / imageHeight); + } + } else { + scale = Math.max(containerWidth / imageWidth, containerHeight / imageHeight); + } + + float paddingLeft = (containerWidth - imageWidth * scale) / 2; + float paddingTop = (containerHeight - imageHeight * scale) / 2; + + mForceClip = paddingLeft < 0 || paddingTop < 0; + + mTransform.setScale(scale, scale); + mTransform.postTranslate(left + paddingLeft, top + paddingTop); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java index 4c75e766f..964dd8464 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java @@ -9,6 +9,8 @@ package com.facebook.react.flat; +import javax.annotation.Nullable; + import android.view.View; import android.view.View.MeasureSpec; @@ -39,14 +41,23 @@ import com.facebook.react.uimanager.ViewManagerRegistry; } /** - * Assigns new DrawCommands to a FlatViewGroup specified by a reactTag. + * Updates DrawCommands and AttachDetachListeners of a FlatViewGroup specified by a reactTag. * * @param reactTag reactTag to lookup FlatViewGroup by - * @param drawCommands new draw commands to execute during the drawing. + * @param drawCommands if non-null, new draw commands to execute during the drawing. + * @param listeners if non-null, new attach-detach listeners. */ - /* package */ void updateMountState(int reactTag, DrawCommand[] drawCommands) { + /* package */ void updateMountState( + int reactTag, + @Nullable DrawCommand[] drawCommands, + @Nullable AttachDetachListener[] listeners) { FlatViewGroup view = (FlatViewGroup) resolveView(reactTag); - view.mountDrawCommands(drawCommands); + if (drawCommands != null) { + view.mountDrawCommands(drawCommands); + } + if (listeners != null) { + view.mountAttachDetachListeners(listeners); + } } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatRootShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatRootShadowNode.java index 2bb93d52d..621284650 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatRootShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatRootShadowNode.java @@ -15,11 +15,13 @@ package com.facebook.react.flat; /* package */ final class FlatRootShadowNode extends FlatShadowNode { private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY; + private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY; private int mViewLeft; private int mViewTop; private int mViewRight; private int mViewBottom; + private boolean mIsUpdated; @Override public int getScreenX() { @@ -41,6 +43,30 @@ package com.facebook.react.flat; return mViewBottom - mViewTop; } + /** + * Returns true when this CSSNode tree needs to be re-laid out. If true, FlatUIImplementation + * will request LayoutEngine to perform a layout pass to update node boundaries. This is used + * to avoid unnecessary node updates. + */ + /* package */ boolean needsLayout() { + return isDirty(); + } + + /** + * Returns true if there are updates to the node tree other than layout (such as a change in + * background color) that would require StateBuilder to re-collect drawing state. + */ + /* package */ boolean isUpdated() { + return mIsUpdated; + } + + /** + * Marks the node tree as requiring or not requiring a StateBuilder pass to collect drawing state. + */ + /* package */ void markUpdated(boolean isUpdated) { + mIsUpdated = isUpdated; + } + /** * Returns an array of DrawCommands to perform during the View's draw pass. */ @@ -56,6 +82,21 @@ package com.facebook.react.flat; mDrawCommands = drawCommands; } + /** + * Sets an array of AttachDetachListeners to call onAttach/onDetach when they are attached to or + * detached from a View that this shadow node maps to. + */ + /* package */ void setAttachDetachListeners(AttachDetachListener[] listeners) { + mAttachDetachListeners = listeners; + } + + /** + * Returns an array of AttachDetachListeners associated with this shadow node. + */ + /* package */ AttachDetachListener[] getAttachDetachListeners() { + return mAttachDetachListeners; + } + /** * Sets boundaries of the View that this node maps to relative to the parent left/top coordinate. */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java index 475533b48..9820553ad 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java @@ -28,4 +28,15 @@ import com.facebook.react.uimanager.LayoutShadowNode; float bottom) { // do nothing yet. } + + /** + * Marks root node as updated to trigger a StateBuilder pass to collect DrawCommands for the node + * tree. Use it when FlatShadowNode is updated but doesn't require a layout pass (e.g. background + * color is changed). + */ + protected final void invalidate() { + // getRootNode() returns an ReactShadowNode, which is guarantied to be a FlatRootShadowNode. + FlatRootShadowNode rootNode = (FlatRootShadowNode) getRootNode(); + rootNode.markUpdated(true); + } } 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 ffe105c27..605aacadf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIImplementation.java @@ -22,6 +22,7 @@ import com.facebook.react.uimanager.UIImplementation; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.uimanager.ViewManagerRegistry; import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.image.ReactImageManager; /** * FlatUIImplementation builds on top of UIImplementation and allows pre-creating everything @@ -36,12 +37,22 @@ public class FlatUIImplementation extends UIImplementation { ReactApplicationContext reactContext, List viewManagers) { + ReactImageManager reactImageManager = findReactImageManager(viewManagers); + if (reactImageManager != null) { + Object callerContext = reactImageManager.getCallerContext(); + if (callerContext != null) { + RCTImageView.setCallerContext(callerContext); + } + } + + TypefaceCache.setAssetManager(reactContext.getAssets()); + viewManagers = new ArrayList(viewManagers); viewManagers.add(new RCTViewManager()); viewManagers.add(new RCTTextManager()); viewManagers.add(new RCTRawTextManager()); viewManagers.add(new RCTVirtualTextManager()); - TypefaceCache.setAssetManager(reactContext.getAssets()); + viewManagers.add(new RCTImageViewManager()); ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers); FlatNativeViewHierarchyManager nativeViewHierarchyManager = new FlatNativeViewHierarchyManager( @@ -131,12 +142,33 @@ public class FlatUIImplementation extends UIImplementation { } } + @Override + protected void calculateRootLayout(ReactShadowNode cssRoot) { + } + @Override protected void applyUpdatesRecursive( ReactShadowNode cssNode, float absoluteX, float absoluteY, EventDispatcher eventDispatcher) { - mStateBuilder.applyUpdates((FlatRootShadowNode) cssNode); + FlatRootShadowNode rootNode = (FlatRootShadowNode) cssNode; + if (!rootNode.needsLayout() && !rootNode.isUpdated()) { + return; + } + + super.calculateRootLayout(rootNode); + rootNode.markUpdated(false); + mStateBuilder.applyUpdates(rootNode); + } + + private static @Nullable ReactImageManager findReactImageManager(List viewManagers) { + for (int i = 0, size = viewManagers.size(); i != size; ++i) { + if (viewManagers.get(i) instanceof ReactImageManager) { + return (ReactImageManager) viewManagers.get(i); + } + } + + return null; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java index 199bc0d6c..6d51b71c0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java @@ -9,6 +9,8 @@ package com.facebook.react.flat; +import javax.annotation.Nullable; + import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.UIViewOperationQueue; @@ -26,16 +28,24 @@ import com.facebook.react.uimanager.UIViewOperationQueue; private final class UpdateMountState implements UIOperation { private final int mReactTag; - private final DrawCommand[] mDrawCommands; + private final @Nullable DrawCommand[] mDrawCommands; + private final @Nullable AttachDetachListener[] mAttachDetachListeners; - private UpdateMountState(int reactTag, DrawCommand[] drawCommands) { + private UpdateMountState( + int reactTag, + @Nullable DrawCommand[] drawCommands, + @Nullable AttachDetachListener[] listeners) { mReactTag = reactTag; mDrawCommands = drawCommands; + mAttachDetachListeners = listeners; } @Override public void execute() { - mNativeViewHierarchyManager.updateMountState(mReactTag, mDrawCommands); + mNativeViewHierarchyManager.updateMountState( + mReactTag, + mDrawCommands, + mAttachDetachListeners); } } @@ -75,8 +85,11 @@ import com.facebook.react.uimanager.UIViewOperationQueue; /** * Enqueues a new UIOperation that will update DrawCommands for a View defined by reactTag. */ - public void enqueueUpdateMountState(int reactTag, DrawCommand[] drawCommands) { - enqueueUIOperation(new UpdateMountState(reactTag, drawCommands)); + public void enqueueUpdateMountState( + int reactTag, + @Nullable DrawCommand[] drawCommands, + @Nullable AttachDetachListener[] listeners) { + enqueueUIOperation(new UpdateMountState(reactTag, drawCommands, listeners)); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java index ab6112892..325850588 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -9,6 +9,10 @@ package com.facebook.react.flat; +import java.lang.ref.WeakReference; + +import javax.annotation.Nullable; + import android.content.Context; import android.graphics.Canvas; import android.view.ViewGroup; @@ -18,8 +22,30 @@ import android.view.ViewGroup; * array of DrawCommands, executing them one by one. */ /* package */ final class FlatViewGroup extends ViewGroup { + /** + * Helper class that allows AttachDetachListener to invalidate the hosting View. + */ + static final class InvalidateCallback extends WeakReference { + private InvalidateCallback(FlatViewGroup view) { + super(view); + } + + /** + * Propagates invalidate() call up to the hosting View (if it's still alive) + */ + public void invalidate() { + FlatViewGroup view = get(); + if (view != null) { + view.invalidate(); + } + } + } + + private @Nullable InvalidateCallback mInvalidateCallback; private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY; + private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY; + private boolean mIsAttached = false; /* package */ FlatViewGroup(Context context) { super(context); @@ -39,8 +65,78 @@ import android.view.ViewGroup; // nothing to do here } + @Override + protected void onAttachedToWindow() { + if (mIsAttached) { + // this is possible, unfortunately. + return; + } + + mIsAttached = true; + + super.onAttachedToWindow(); + dispatchOnAttached(mAttachDetachListeners); + } + + @Override + protected void onDetachedFromWindow() { + if (!mIsAttached) { + throw new RuntimeException("Double detach"); + } + + mIsAttached = false; + + super.onDetachedFromWindow(); + dispatchOnDetached(mAttachDetachListeners); + } + /* package */ void mountDrawCommands(DrawCommand[] drawCommands) { mDrawCommands = drawCommands; invalidate(); } + + /* package */ void mountAttachDetachListeners(AttachDetachListener[] listeners) { + if (mIsAttached) { + // Ordering of the following 2 statements is very important. While logically it makes sense to + // detach old listeners first, and only then attach new listeners, this is not very efficient, + // because a listener can be in both lists. In this case, it will be detached first and then + // re-attached immediately. This is undesirable for a couple of reasons: + // 1) performance. Detaching is slow because it may cancel an ongoing network request + // 2) it may cause flicker: an image that was already loaded may get unloaded. + // + // For this reason, we are attaching new listeners first. What this means is that listeners + // that are in both lists need to gracefully handle a secondary attach and detach events, + // (i.e. onAttach() being called when already attached, followed by a detach that should be + // ignored) turning them into no-ops. This will result in no performance loss and no flicker, + // because ongoing network requests don't get cancelled. + dispatchOnAttached(listeners); + dispatchOnDetached(mAttachDetachListeners); + } + mAttachDetachListeners = listeners; + } + + private void dispatchOnAttached(AttachDetachListener[] listeners) { + int numListeners = listeners.length; + if (numListeners == 0) { + return; + } + + InvalidateCallback callback = getInvalidateCallback(); + for (AttachDetachListener listener : listeners) { + listener.onAttached(callback); + } + } + + private InvalidateCallback getInvalidateCallback() { + if (mInvalidateCallback == null) { + mInvalidateCallback = new InvalidateCallback(this); + } + return mInvalidateCallback; + } + + private static void dispatchOnDetached(AttachDetachListener[] listeners) { + for (AttachDetachListener listener : listeners) { + listener.onDetached(); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java new file mode 100644 index 000000000..3a019e18b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java @@ -0,0 +1,117 @@ +/** + * 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.content.Context; +import android.content.res.Resources; +import android.net.Uri; + +import com.facebook.drawee.drawable.ScalingUtils.ScaleType; +import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.facebook.react.uimanager.ReactProp; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.views.image.ImageResizeMode; + +/** + * RCTImageView is a top-level node for Image. It can display either a remote image + * (source must start wtih http:// or https://) or a local resource (a BitmapDrawable). + */ +/* package */ class RCTImageView extends FlatShadowNode { + + static Object sCallerContext = RCTImageView.class; + + /** + * Assignes a CallerContext to execute network requests with. + */ + /* package */ static void setCallerContext(Object callerContext) { + sCallerContext = callerContext; + } + + /* package */ static Object getCallerContext() { + return sCallerContext; + } + + private T mDrawImage; + + /* package */ RCTImageView(T drawImage) { + mDrawImage = drawImage; + } + + @Override + protected void collectState( + StateBuilder stateBuilder, + float left, + float top, + float right, + float bottom) { + super.collectState(stateBuilder, left, top, right, bottom); + + if (mDrawImage.hasImageRequest()) { + mDrawImage = (T) mDrawImage.updateBoundsAndFreeze( + left, + top, + right, + bottom); + stateBuilder.addDrawCommand(mDrawImage); + stateBuilder.addAttachDetachListener(mDrawImage); + } + } + + @ReactProp(name = "src") + public void setSource(@Nullable String source) { + if (source == null) { + getMutableDrawImage().setImageRequest(null); + return; + } + + final ImageRequestBuilder imageRequestBuilder; + if (isNetworkResource(source)) { + imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(source)); + } else { + Context context = getThemedContext(); + Resources resources = context.getResources(); + int resId = resources.getIdentifier( + source, + "drawable", + context.getPackageName()); + imageRequestBuilder = ImageRequestBuilder.newBuilderWithResourceId(resId); + } + + getMutableDrawImage().setImageRequest(imageRequestBuilder.build()); + } + + @ReactProp(name = "tintColor") + public void setTintColor(int tintColor) { + getMutableDrawImage().setTintColor(tintColor); + } + + @ReactProp(name = ViewProps.RESIZE_MODE) + public void setResizeMode(@Nullable String resizeMode) { + ScaleType scaleType = ImageResizeMode.toScaleType(resizeMode); + if (mDrawImage.getScaleType() != scaleType) { + getMutableDrawImage().setScaleType(scaleType); + } + } + + private T getMutableDrawImage() { + if (mDrawImage.isFrozen()) { + mDrawImage = (T) mDrawImage.mutableCopy(); + invalidate(); + } + + return mDrawImage; + } + + private static boolean isNetworkResource(String source) { + return source.startsWith("http://") || source.startsWith("https://"); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageViewManager.java new file mode 100644 index 000000000..b1667ef76 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageViewManager.java @@ -0,0 +1,28 @@ +/** + * 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; + +/* package */ final class RCTImageViewManager extends VirtualViewManager { + + @Override + public String getName() { + return "RCTImageView"; + } + + @Override + public RCTImageView createShadowNodeInstance() { + return new RCTImageView(new DrawImageWithPipeline()); + } + + @Override + public Class getShadowNodeClass() { + return RCTImageView.class; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java index eec17d3e7..1e0c7b69e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java @@ -20,7 +20,9 @@ package com.facebook.react.flat; private final FlatUIViewOperationQueue mOperationsQueue; private final ElementsList mDrawCommands = - new ElementsList(DrawCommand.EMPTY_ARRAY); + new ElementsList<>(DrawCommand.EMPTY_ARRAY); + private final ElementsList mAttachDetachListeners = + new ElementsList<>(AttachDetachListener.EMPTY_ARRAY); /* package */ StateBuilder(FlatUIViewOperationQueue operationsQueue) { mOperationsQueue = operationsQueue; @@ -41,6 +43,10 @@ package com.facebook.react.flat; mDrawCommands.add(drawCommand); } + /* package */ void addAttachDetachListener(AttachDetachListener listener) { + mAttachDetachListeners.add(listener); + } + /** * Updates boundaries of a View that a give nodes maps to. */ @@ -76,15 +82,25 @@ package com.facebook.react.flat; float width, float height) { mDrawCommands.start(node.getDrawCommands()); + mAttachDetachListeners.start(node.getAttachDetachListeners()); collectStateRecursively(node, 0, 0, width, height); + boolean shouldUpdateMountState = false; final DrawCommand[] drawCommands = mDrawCommands.finish(); if (drawCommands != null) { - // DrawCommands changed, need to re-mount them and re-draw the View. + shouldUpdateMountState = true; node.setDrawCommands(drawCommands); + } - mOperationsQueue.enqueueUpdateMountState(tag, drawCommands); + final AttachDetachListener[] listeners = mAttachDetachListeners.finish(); + if (listeners != null) { + shouldUpdateMountState = true; + node.setAttachDetachListeners(listeners); + } + + if (shouldUpdateMountState) { + mOperationsQueue.enqueueUpdateMountState(tag, drawCommands, listeners); } }