diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java index b0e4e111f9..b09b051665 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java @@ -27,8 +27,123 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.views.view.ReactClippingViewGroupHelper; /** - * Implementation of a {@link DrawCommandManager} with clipping. Performs drawing by iterating - * over an array of DrawCommands, executing them one by one except when the commands are clipped. + * Abstract class for a {@link DrawCommandManager} with directional clipping. Allows support for + * vertical and horizontal clipping by implementing abstract methods. + * + * Uses two dynamic programming arrays to efficiently update which views and commands are onscreen, + * while not having to sort the incoming draw commands. The draw commands are loosely sorted, as + * they represent a flattening of the normal view hierarchy, and we use that information to quickly + * find children that should be considered onscreen. One array keeps track of, for each index, the + * maximum bottom position that occurs at or before that index; the other keeps track of the + * minimum top position that occurs at or after that index. Given the following children: + * + * +---------------------------------+ 0 (Y coordinate) + * | 0 | + * | +-----------+ | 10 + * | | 1 | | + * | | | +--------------+ | 20 + * | | | | 3 | | + * | +-----------+ | | | 30 + * | | | | + * | +-----------+ | | | 40 + * | | 2 | | | | + * | | | +--------------+ | 50 + * | | | | + * | +-----------+ | 60 + * | | + * +---------------------------------+ 70 + * + * +-----------+ 80 + * | 4 | + * | | +--------------+ 90 + * | | | 6 | + * +-----------+ | | 100 + * | | + * +-----------+ | | 110 + * | 5 | | | + * | | +--------------+ 120 + * | | + * +-----------+ 130 + * + * The two arrays are: + * 0 1 2 3 4 5 6 + * Max Bottom: [70, 70, 70, 70, 100, 130, 130] + * Min Top: [ 0, 0, 0, 0, 80, 90, 90] + * + * We can then binary search for the first max bottom that is below our rect, and the first min top + * that is above our rect. + * + * If the top and bottom of the rect are 55 and 85, respectively, we will start drawing at index 0 + * and stop at index 4. + * + * +---------------------------------+ 0 (Y coordinate) + * | 0 | + * | +-----------+ | 10 + * | | 1 | | + * | | | +--------------+ | 20 + * | | | | 3 | | + * | +-----------+ | | | 30 + * | | | | + * | +-----------+ | | | 40 + * | | 2 | | | | + * | | | +--------------+ | 50 + * - -| -| - - - -| - - - - - - |- - - + * | +-----------+ | 60 + * | | + * +---------------------------------+ 70 + * + * +-----------+ 80 + * - - -| 4 - - - | - - - - - - - - - + * | | +--------------+ 90 + * | | | 6 | + * +-----------+ | | 100 + * | | + * +-----------+ | | 110 + * | 5 | | | + * | | +--------------+ 120 + * | | + * +-----------+ 130 + * + * If the top and bottom are 75 and 105 respectively, we will start drawing at index 4 and stop at + * index 6. + * + * +---------------------------------+ 0 (Y coordinate) + * | 0 | + * | +-----------+ | 10 + * | | 1 | | + * | | | +--------------+ | 20 + * | | | | 3 | | + * | +-----------+ | | | 30 + * | | | | + * | +-----------+ | | | 40 + * | | 2 | | | | + * | | | +--------------+ | 50 + * | | | | + * | +-----------+ | 60 + * | | + * +---------------------------------+ 70 + * - - - - - - - - - - - - - - - - + * +-----------+ 80 + * | 4 | + * | | +--------------+ 90 + * | | | 6 | + * +-----------+ | | 100 + * - - - - - - - |- - - - - | - - - + * +-----------+ | | 110 + * | 5 | | | + * | | +--------------+ 120 + * | | + * +-----------+ 130 + * + * While this doesn't map exactly to all of the commands that could be clipped, it means that + * children which contain other children (a pretty common case when flattening views) are clipped + * or unclipped as one logical unit. This has the side effect of minimizing the amount of + * invalidates coming from minor clipping rect adjustments. The underlying dynamic programming + * arrays can be calculated off the UI thread in O(n) time, requiring just two passes through the + * command array. + * + * We do a similar optimization when searching for matching node regions, as node regions are + * loosely sorted as well when clipping. */ /* package */ abstract class ClippingDrawCommandManager extends DrawCommandManager { private final FlatViewGroup mFlatViewGroup; @@ -63,6 +178,13 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; initialSetup(drawCommands); } + /** + * Initially setup this instance. Makes sure the draw commands are mounted, and that our + * clipping rect reflects our current bounds. + * + * @param drawCommands The list of current draw commands. In current implementations, this will + * always be DrawCommand.EMPTY_ARRAY + */ private void initialSetup(DrawCommand[] drawCommands) { mountDrawCommands( drawCommands, @@ -255,14 +377,18 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; } } - // Returns true if a view is currently animating. + /** + * Returns true if a view is currently animating. + */ private static boolean animating(View view) { Animation animation = view.getAnimation(); return animation != null && !animation.hasEnded(); } - // Return true if a command index is currently onscreen. - boolean withinBounds(int i) { + /** + * Returns true if a command index is currently onscreen. + */ + private boolean withinBounds(int i) { return mStart <= i && i < mStop; } @@ -291,6 +417,22 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; return true; } + /** + * Used either after we have updated the current rect, or when we have mounted new commands and + * the rect hasn't changed. Updates the clipping after mStart and mStop have been set to the + * correct values. For draw commands, this is all it takes to update the command mounting, as + * draw commands are only attached in a conceptual sense, and rely on the android view + * hierarchy. + * + * For native children, we have to walk through our current views and remove any that are no + * longer on screen, and add those that are newly on screen. As an optimization for fling, if we + * are removing two or more native views we instead detachAllViews from the {@link FlatViewGroup} + * and re-attach or add as needed. + * + * This approximation is roughly correct, as we tend to add and remove the same amount of views, + * and each add and remove pair is O(n); detachAllViews and re-attach requires two passes, so + * using this once we are removing more than two native views is a good breakpoint. + */ private void updateClippingToCurrentRect() { for (int i = 0, size = mFlatViewGroup.getChildCount(); i < size; i++) { View view = mFlatViewGroup.getChildAt(i); @@ -372,6 +514,47 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; return mClippedSubviews.values(); } + /** + * Draws the unclipped commands on the given canvas. This would be much simpler if we didn't + * have to worry about animating views, as we could simply: + * + * for (int i = start; i < stop; i++) { + * drawCommands[i].draw(...); + * } + * + * This is complicated however by animating views, which may occur before or after the current + * clipping rect. Consider the following array: + * + * +--------------+ + * | DrawView | 0 + * | *animating* | + * +--------------+ + * | DrawCommmand | 1 + * | *clipped* | + * +--------------+ + * | DrawCommand | 2 start + * | | + * +--------------+ + * | DrawCommand | 3 + * | | + * +--------------+ + * | DrawView | 4 + * | | + * +--------------+ + * | DrawView | 5 stop + * | *clipped* | + * +--------------+ + * | DrawView | 6 + * | *animating* | + * +--------------+ + * + * 2, 3, and 4 are onscreen according to bounds, while 0 and 6 are onscreen according to + * animation. We have to walk through the attached children making sure to draw any draw + * commands that should be drawn before that draw view, as well as making sure not to draw any + * draw commands that are out of bounds. + * + * @param canvas The canvas to draw on. + */ @Override public void draw(Canvas canvas) { int commandIndex = mStart; diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java index 3df3c69e6c..f87e82596f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java @@ -20,7 +20,8 @@ import android.view.View; import android.view.ViewParent; /** - * Underlying logic behind handling clipping draw commands from {@link FlatViewGroup}. + * Underlying logic which handles draw commands, views and node regions when clipping in a + * {@link FlatViewGroup}. */ /* package */ abstract class DrawCommandManager { @@ -136,6 +137,13 @@ import android.view.ViewParent; } } + /** + * Get a draw command manager that will clip vertically (The view scrolls up and down). + * + * @param flatViewGroup FlatViewGroup to use for drawing. + * @param drawCommands List of commands to mount. + * @return Vertically clipping draw command manager. + */ static DrawCommandManager getVerticalClippingInstance( FlatViewGroup flatViewGroup, DrawCommand[] drawCommands) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithDrawee.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithDrawee.java index 28ed3b1c7c..c458e90fca 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithDrawee.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithDrawee.java @@ -39,7 +39,7 @@ import com.facebook.react.views.imagehelper.MultiSourceHelper; import com.facebook.react.views.imagehelper.MultiSourceHelper.MultiSourceResult; /** - * DrawImageWithDrawee is DrawCommand that can draw a local or remote image. + * DrawImageWithDrawee is a DrawCommand that can draw a local or remote image. * It uses DraweeRequestHelper internally to fetch and cache the images. */ /* package */ final class DrawImageWithDrawee extends AbstractDrawCommand diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/ElementsList.java b/ReactAndroid/src/main/java/com/facebook/react/flat/ElementsList.java index e77b42599b..b0e5eb8685 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/ElementsList.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/ElementsList.java @@ -14,34 +14,49 @@ import java.util.ArrayList; import java.lang.reflect.Array; /** - * Helper class that supports 3 main operations: start(), add() an element and finish(). + * Diffing scope stack class that supports 3 main operations: start(), add() an element and + * finish(). * * When started, it takes a baseline array to compare to. When adding a new element, it checks * whether a corresponding element in baseline array is the same. On finish(), it will return null * if baseline array contains exactly the same elements that were added with a sequence of add() - * calls, or a new array the recorded elements: + * calls, or a new array of the recorded elements: * * Example 1: * ----- - * start([A]) - * add(A) - * finish() -> null (because [A] == [A]) + * start([A]) + * add(A) + * finish() -> null (because [A] == [A]) * * Example 2: * ---- - * start([A]) - * add(B) - * finish() -> [B] (because [A] != [B]) + * start([A]) + * add(B) + * finish() -> [B] (because [A] != [B]) + * + * Example 3: + * ---- + * start([A]) + * add(B) + * add(A) + * finish() -> [B, A] (because [B, A] != [A]) + * + * Example 4: + * ---- + * start([A, B]) + * add(B) + * add(A) + * finish() -> [B, A] (because [B, A] != [A, B]) * * It is important that start/finish can be nested: * ---- - * start([A]) - * add(A) - * start([B]) - * add(B) - * finish() -> null - * add(C) - * finish() -> [A, C] + * start([A]) + * add(A) + * start([B]) + * add(B) + * finish() -> null + * add(C) + * finish() -> [A, C] * * StateBuilder is using this class to check if e.g. a DrawCommand list for a given View needs to be * updated. @@ -54,7 +69,12 @@ import java.lang.reflect.Array; int size; } + // List of scopes. These are never cleared, but instead recycled when a new scope is needed at + // a given depth. private final ArrayList mScopesStack = new ArrayList<>(); + // Working list of all new elements we are gathering across scopes. Whenever we get a call to + // finish() we pop the new elements off the collection, either discarding them if there was no + // change from the base or accumulating and returning them as a list of new elements. private final ArrayDeque mElements = new ArrayDeque<>(); private final E[] mEmptyArray; private Scope mCurrentScope = null; @@ -78,8 +98,8 @@ import java.lang.reflect.Array; } /** - * Finished current scope, and returns null if there were no changes recorded, or a new array - * containing all the recorded elements otherwise. + * Finish current scope, returning null if there were no changes recorded, or a new array + * containing all the newly recorded elements otherwise. */ public E[] finish() { Scope scope = getCurrentScope(); @@ -96,14 +116,15 @@ import java.lang.reflect.Array; } } - // to prevent leaks + // To prevent resource leaks. scope.elements = null; return result; } /** - * Adds a new element to the list. This method can be optimized to avoid inserts on same elements. + * Adds a new element to the list. This method can be optimized to avoid inserts on same + * elements, but would involve copying from scope.elements when we extract elements. */ public void add(E element) { Scope scope = getCurrentScope(); @@ -119,7 +140,7 @@ import java.lang.reflect.Array; } /** - * Resets all references to the elements to null to avoid memory leaks. + * Resets all references to elements in our new stack to null to avoid memory leaks. */ public void clear() { if (getCurrentScope() != null) { @@ -129,7 +150,8 @@ import java.lang.reflect.Array; } /** - * Extracts last size elements into an array. + * Extracts last size elements into an array. Used to extract our new array of items from our + * stack when the new items != old items. */ private E[] extractElements(int size) { if (size == 0) { @@ -151,15 +173,18 @@ import java.lang.reflect.Array; private void pushScope() { ++mScopeIndex; if (mScopeIndex == mScopesStack.size()) { + // We reached a new deepest scope, we need to create a scope for this depth. mCurrentScope = new Scope(); mScopesStack.add(mCurrentScope); } else { + // We have had a scope at this depth before, lets recycle it. mCurrentScope = mScopesStack.get(mScopeIndex); } } /** - * Restores last save current scope. + * Restores last saved current scope. Doesn't actually remove the scope, as scopes are + * recycled. */ private void popScope() { --mScopeIndex; 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 3b231d1e79..1675ef147f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -42,16 +42,64 @@ import com.facebook.react.views.image.ImageLoadEvent; import com.facebook.react.views.view.ReactClippingViewGroup; /** - * A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over - * array of DrawCommands, executing them one by one. In the case of clipping, the underlying logic - * is handled by {@link DrawCommandManager}. This lets us separate logic, while also allowing us - * to save on memory for data structures only used in clipping. + * A view that the {@link FlatShadowNode} hierarchy maps to. Can mount and draw native views as + * well as draw commands. We reuse some of Android's ViewGroup logic, but in Nodes we try to + * minimize the amount of shadow nodes that map to native children, so we have a lot of logic + * specific to draw commands. + * + * In a very simple case with no Android children, the FlatViewGroup will receive: + * + * flatViewGroup.mountDrawCommands(...); + * flatViewGroup.dispatchDraw(...); + * + * The draw commands are mounted, then draw iterates through and draws them one by one. + * + * In a simple case where there are native children: + * + * flatViewGroup.mountDrawCommands(...); + * flatViewGroup.detachAllViewsFromParent(...); + * flatViewGroup.mountViews(...); + * flatViewGroup.dispatchDraw(...); + * + * Draw commands are mounted, with a draw view command for each mounted view. As an optimization + * we then detach all views from the FlatViewGroup, then allow mountViews to selectively reattach + * and add views in order. We do this as adding a single view is a O(n) operation (On average you + * have to move all the views in the array to the right one position), as is dropping and re-adding + * all views (One pass to clear the array and one pass to re-attach detached children and add new + * children). + * + * FlatViewGroups also have arrays of node regions, which are little more than a rects that + * represents a touch target. Native views contain their own touch logic, but not all react tags + * map to native views. We use node regions to find touch targets among commands as well as nodes + * which map to native views. + * + * In the case of clipping, much of the underlying logic for is handled by + * {@link DrawCommandManager}. This lets us separate logic, while also allowing us to save on + * memory for data structures only used in clipping. In a case of a clipping FlatViewGroup which + * is scrolling: + * + * flatViewGroup.setRemoveClippedSubviews(true); + * flatViewGroup.mountClippingDrawCommands(...); + * flatViewGroup.detachAllViewsFromParent(...); + * flatViewGroup.mountViews(...); + * flatViewGroup.updateClippingRect(...); + * flatViewGroup.dispatchDraw(...); + * flatViewGroup.updateClippingRect(...); + * flatViewGroup.dispatchDraw(...); + * flatViewGroup.updateClippingRect(...); + * flatViewGroup.dispatchDraw(...); + * + * Setting remove clipped subviews creates a {@link DrawCommandManager} to handle clipping, which + * allows the rest of the methods to simply call in to draw command manager to handle the clipping + * logic. */ /* package */ final class FlatViewGroup extends ViewGroup implements ReactInterceptingViewGroup, ReactClippingViewGroup, ReactCompoundViewGroup, ReactHitSlopView, ReactPointerEventsView, FlatMeasuredViewGroup { /** - * Helper class that allows AttachDetachListener to invalidate the hosting View. + * Helper class that allows our AttachDetachListeners to invalidate the hosting View. When a + * listener gets an attach it is passed an invalidate callback for the FlatViewGroup it is being + * attached to. */ static final class InvalidateCallback extends WeakReference { @@ -69,6 +117,12 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } } + /** + * Propogates image load events to javascript if the hosting view is still alive. + * + * @param reactTag The view id. + * @param imageLoadEvent The event type. + */ public void dispatchImageLoadEvent(int reactTag, int imageLoadEvent) { FlatViewGroup view = get(); if (view == null) { @@ -82,6 +136,7 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } } + // Resources for debug drawing. private static final boolean DEBUG_DRAW = false; private static final boolean DEBUG_DRAW_TEXT = false; private boolean mAndroidDebugDraw; @@ -94,10 +149,14 @@ import com.facebook.react.views.view.ReactClippingViewGroup; private static final ArrayList LAYOUT_REQUESTS = new ArrayList<>(); private static final Rect VIEW_BOUNDS = new Rect(); + // An invalidate callback singleton for this FlatViewGroup. private @Nullable InvalidateCallback mInvalidateCallback; private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY; private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY; private NodeRegion[] mNodeRegions = NodeRegion.EMPTY_ARRAY; + + // The index of the next native child to draw. This is used in dispatchDraw to check that we are + // actually drawing all of our attached children, then is reset to 0. private int mDrawChildIndex = 0; private boolean mIsAttached = false; private boolean mIsLayoutRequested = false; @@ -108,6 +167,7 @@ import com.facebook.react.views.view.ReactClippingViewGroup; private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; private static final ArrayList EMPTY_DETACHED_VIEWS = new ArrayList<>(0); + // Provides clipping, drawing and node region finding logic if subview clipping is enabled. private @Nullable DrawCommandManager mDrawCommandManager; private @Nullable Rect mHitSlopRect; @@ -165,12 +225,22 @@ import com.facebook.react.views.view.ReactClippingViewGroup; return nodeRegion != null && nodeRegion.mIsVirtual; } - // This is hidden in the Android ViewGroup, but still gets called in super.dispatchDraw. + /** + * Secretly Overrides the hidden ViewGroup.onDebugDraw method. This is hidden in the Android + * ViewGroup, but still gets called in super.dispatchDraw. Overriding here allows us to draw + * layout bounds for Nodes when android is drawing layout bounds. + */ protected void onDebugDraw(Canvas canvas) { // Android is drawing layout bounds, so we should as well. mAndroidDebugDraw = true; } + /** + * Draw FlatViewGroup on a canvas. Also checks that all children are drawn, as a draw view calls + * back to the FlatViewGroup to draw each child. + * + * @param canvas The canvas to draw on. + */ @Override public void dispatchDraw(Canvas canvas) { mAndroidDebugDraw = false; @@ -217,6 +287,38 @@ import com.facebook.react.views.view.ReactClippingViewGroup; mDrawChildIndex = 0; } + /** + * This override exists to suppress the default drawing behaviour of the ViewGroup. dispatchDraw + * calls super.dispatchDraw, which lets Android perform some of our child management logic. + * super.dispatchDraw then calls our drawChild, which is suppressed. + * + * dispatchDraw within the FlatViewGroup then calls super.drawChild, which actually draws the + * child. + * + * // Pseudocode example. + * Class FlatViewGroup { + * void dispatchDraw() { + * super.dispatchDraw(); // Eventually calls our drawChild, which is a no op. + * super.drawChild(); // Calls the actual drawChild. + * } + * + * boolean drawChild(...) { + * // No op. + * } + * } + * + * Class ViewGroup { + * void dispatchDraw() { + * drawChild(); // No op. + * } + * + * boolean drawChild(...) { + * getChildAt(...).draw(); + * } + * } + * + * @return false, as we are suppressing drawChild. + */ @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // suppress @@ -224,6 +326,11 @@ import com.facebook.react.views.view.ReactClippingViewGroup; return false; } + /** + * Draw layout bounds for the next child. + * + * @param canvas The canvas to draw on. + */ /* package */ void debugDrawNextChild(Canvas canvas) { View child = getChildAt(mDrawChildIndex); // Draw FlatViewGroups a different color than regular child views. @@ -291,6 +398,10 @@ import com.facebook.react.views.view.ReactClippingViewGroup; drawCorner(canvas, paint, x2, y2, -lineLength, -lineLength, lineWidth); } + /** + * Makes sure that we only initialize one instance of each of our layout bounds drawing + * resources. + */ private void initDebugDrawResources() { if (sDebugTextPaint == null) { sDebugTextPaint = new Paint(); @@ -320,6 +431,17 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } } + /** + * Used in drawing layout bounds, draws a layout bounds rectangle similar to the Android default + * implementation, with a specifiable border color. + * + * @param canvas The canvas to draw on. + * @param color The border color of the layout bounds. + * @param left Left bound of the rectangle. + * @param top Top bound of the rectangle. + * @param right Right bound of the rectangle. + * @param bottom Bottom bound of the rectangle. + */ private void debugDrawRect( Canvas canvas, int color, @@ -330,6 +452,19 @@ import com.facebook.react.views.view.ReactClippingViewGroup; debugDrawNamedRect(canvas, color, "", left, top, right, bottom); } + /** + * Used in drawing layout bounds, draws a layout bounds rectangle similar to the Android default + * implementation, with a specifiable border color. Also draws a name text in the bottom right + * corner of the rectangle if DEBUG_DRAW_TEXT is set. + * + * @param canvas The canvas to draw on. + * @param color The border color of the layout bounds. + * @param name Name to be drawn on top of the rectangle if DEBUG_DRAW_TEXT is set. + * @param left Left bound of the rectangle. + * @param top Top bound of the rectangle. + * @param right Right bound of the rectangle. + * @param bottom Bottom bound of the rectangle. + */ /* package */ void debugDrawNamedRect( Canvas canvas, int color, @@ -385,7 +520,7 @@ import com.facebook.react.views.view.ReactClippingViewGroup; @Override protected void onAttachedToWindow() { if (mIsAttached) { - // this is possible, unfortunately. + // This is possible, unfortunately. return; } @@ -552,6 +687,12 @@ import com.facebook.react.views.view.ReactClippingViewGroup; invalidate(); } + /** + * Draws the next child of the FlatViewGroup. Each draw view calls FlatViewGroup.drawNextChild, + * which keeps track of the current child index to draw. + * + * @param canvas The canvas to draw on. + */ /* package */ void drawNextChild(Canvas canvas) { View child = getChildAt(mDrawChildIndex); if (child instanceof FlatViewGroup) { @@ -568,11 +709,38 @@ import com.facebook.react.views.view.ReactClippingViewGroup; ++mDrawChildIndex; } + /** + * Mount a list of draw commands to this FlatViewGroup. Draw commands sometimes map to a view, + * as in the case of {@link DrawView}, and sometimes to a simple canvas operation. We only + * receive a call to mount draw commands when our commands have changed, so we always invalidate. + * + * A call to mount draw commands will only be followed by a call to mount views if the draw view + * commands within the draw command array have changed since last mount. + * + * @param drawCommands The draw commands to mount. + */ /* package */ void mountDrawCommands(DrawCommand[] drawCommands) { mDrawCommands = drawCommands; invalidate(); } + /** + * Mount a list of draw commands to this FlatViewGroup, which is clipping subviews. Clipping + * logic is handled by a {@link DrawCommandManager}, which provides a better explanation of + * these arguments and logic. + * + * A call to mount draw commands will only be followed by a call to mount views if the draw view + * commands within the draw command array have changed since last mount, which is indicated here + * by willMountViews. + * + * @param drawCommands The draw commands to mount. + * @param drawViewIndexMap See {@link DrawCommandManager}. + * @param maxBottom See {@link DrawCommandManager}. + * @param minTop See {@link DrawCommandManager}. + * @param willMountViews True if we will also receive a mountViews call. If we are going to + * receive a call to mount views, that will take care of updating the commands that are + * currently onscreen, otherwise we need to update the onscreen commands. + */ /* package */ void mountClippingDrawCommands( DrawCommand[] drawCommands, SparseIntArray drawViewIndexMap, @@ -589,9 +757,10 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } /** - * Finds a NodeRegion which matches the said reactTag - * @param reactTag the reactTag to look for - * @return the NodeRegion, or NodeRegion.EMPTY + * Return the NodeRegion which matches a reactTag, or EMPTY if none match. + * + * @param reactTag The reactTag to look for + * @return The matching NodeRegion, or NodeRegion.EMPTY if none match. */ /* package */ NodeRegion getNodeRegionForTag(int reactTag) { for (NodeRegion region : mNodeRegions) { @@ -607,7 +776,7 @@ import com.facebook.react.views.view.ReactClippingViewGroup; * strong reference to. This is used by the FlatNativeViewHierarchyManager to explicitly clean up * those views when removing this parent. * - * @return a Collection of Views to clean up + * @return A Collection of Views to clean up. */ Collection getDetachedViews() { if (mDrawCommandManager == null) { @@ -618,9 +787,10 @@ import com.facebook.react.views.view.ReactClippingViewGroup; /** * Remove the detached view from the parent - * This is used during cleanup to trigger onDetachedFromWindow on any views that were in a - * temporary detached state due to them being clipped. This is called for cleanup of said views - * by FlatNativeViewHierarchyManager. + * This is used in the DrawCommandManagers and during cleanup to trigger onDetachedFromWindow on + * any views that were in a temporary detached state due to them being clipped. This is called + * for cleanup of said views by FlatNativeViewHierarchyManager. + * * @param view the detached View to remove */ void removeDetachedView(View view) { @@ -637,6 +807,16 @@ import com.facebook.react.views.view.ReactClippingViewGroup; super.removeAllViewsInLayout(); } + /** + * Mounts attach detach listeners to a FlatViewGroup. The Nodes spec states that children and + * commands deal gracefully with multiple attaches and detaches, and as long as: + * + * attachCount - detachCount > 0 + * + * Then children still consider themselves as attached. + * + * @param listeners The listeners to mount. + */ /* package */ void mountAttachDetachListeners(AttachDetachListener[] listeners) { if (mIsAttached) { // Ordering of the following 2 statements is very important. While logically it makes sense to @@ -657,10 +837,25 @@ import com.facebook.react.views.view.ReactClippingViewGroup; mAttachDetachListeners = listeners; } + /** + * Mount node regions to a FlatViewGroup. A node region is a touch target for a react tag. As + * not all react tags map to a view, we use node regions to determine whether a non-native region + * should receive a touch. + * + * @param nodeRegions The node regions to mount. + */ /* package */ void mountNodeRegions(NodeRegion[] nodeRegions) { mNodeRegions = nodeRegions; } + /** + * Mount node regions in clipping. See {@link DrawCommandManager} for more complete + * documentation. + * + * @param nodeRegions The node regions to mount. + * @param maxBottom See {@link DrawCommandManager}. + * @param minTop See {@link DrawCommandManager}. + */ /* package */ void mountClippingNodeRegions( NodeRegion[] nodeRegions, float[] maxBottom, @@ -723,18 +918,40 @@ import com.facebook.react.views.view.ReactClippingViewGroup; invalidate(); } + /** + * Exposes the protected addViewInLayout call for the {@link DrawCommandManager}. + * + * @param view The view to add. + */ /* package */ void addViewInLayout(View view) { addViewInLayout(view, -1, ensureLayoutParams(view.getLayoutParams()), true); } + /** + * Exposes the protected addViewInLayout call for the {@link DrawCommandManager}. + * + * @param view The view to add. + * @param index The index position at which to add this child. + */ /* package */ void addViewInLayout(View view, int index) { addViewInLayout(view, index, ensureLayoutParams(view.getLayoutParams()), true); } + /** + * Exposes the protected attachViewToParent call for the {@link DrawCommandManager}. + * + * @param view The view to attach. + */ /* package */ void attachViewToParent(View view) { attachViewToParent(view, -1, ensureLayoutParams(view.getLayoutParams())); } + /** + * Exposes the protected attachViewToParent call for the {@link DrawCommandManager}. + * + * @param view The view to attach. + * @param index The index position at which to attach this child. + */ /* package */ void attachViewToParent(View view, int index) { attachViewToParent(view, index, ensureLayoutParams(view.getLayoutParams())); } @@ -754,6 +971,10 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } } + /** + * Called after the view hierarchy is updated in {@link StateBuilder}, to process all the + * FlatViewGroups that have requested layout. + */ /* package */ static void processLayoutRequests() { for (int i = 0, numLayoutRequests = LAYOUT_REQUESTS.size(); i != numLayoutRequests; ++i) { FlatViewGroup flatViewGroup = LAYOUT_REQUESTS.get(i); @@ -795,6 +1016,14 @@ import com.facebook.react.views.view.ReactClippingViewGroup; return new Rect(left, top, right, bottom); } + /** + * Searches for a virtual node region matching the specified x and y touch. Virtual in this case + * means simply that the node region represents a command, rather than a native view. + * + * @param touchX The touch x coordinate. + * @param touchY The touch y coordinate. + * @return A virtual node region matching the specified touch, or null if no regions match. + */ private @Nullable NodeRegion virtualNodeRegionWithinBounds(float touchX, float touchY) { if (mDrawCommandManager != null) { return mDrawCommandManager.virtualNodeRegionWithinBounds(touchX, touchY); @@ -813,6 +1042,14 @@ import com.facebook.react.views.view.ReactClippingViewGroup; return null; } + /** + * Searches for a node region matching the specified x and y touch. Will search regions which + * representing both commands and native views. + * + * @param touchX The touch x coordinate. + * @param touchY The touch y coordinate. + * @return A node region matching the specified touch, or null if no regions match. + */ private @Nullable NodeRegion anyNodeRegionWithinBounds(float touchX, float touchY) { if (mDrawCommandManager != null) { return mDrawCommandManager.anyNodeRegionWithinBounds(touchX, touchY); @@ -835,6 +1072,11 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } } + /** + * Propagate attach to a list of listeners, passing a callback by which they can invalidate. + * + * @param listeners List of listeners to attach. + */ private void dispatchOnAttached(AttachDetachListener[] listeners) { int numListeners = listeners.length; if (numListeners == 0) { @@ -847,6 +1089,11 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } } + /** + * Get an invalidate callback singleton for this view instance. + * + * @return Invalidate callback singleton. + */ private InvalidateCallback getInvalidateCallback() { if (mInvalidateCallback == null) { mInvalidateCallback = new InvalidateCallback(this); @@ -854,6 +1101,11 @@ import com.facebook.react.views.view.ReactClippingViewGroup; return mInvalidateCallback; } + /** + * Propagate detach to a list of listeners. + * + * @param listeners List of listeners to detach. + */ private static void dispatchOnDetached(AttachDetachListener[] listeners) { for (AttachDetachListener listener : listeners) { listener.onDetached(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java index 66602619b5..2b0be03629 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java @@ -57,24 +57,44 @@ import android.util.SparseIntArray; return mRegionMaxBottom[index] < touchX; } - // These should never be called from the UI thread, as the reason they exist is to do work off the - // UI thread. - public static void fillMaxMinArrays(NodeRegion[] regions, float[] maxBottom, float[] minTop) { + /** + * Populates the max and min arrays for a given set of node regions. + * + * This should never be called from the UI thread, as the reason it exists is to do work off the + * UI thread. + * + * @param regions The regions that will eventually be mounted. + * @param maxRight At each index i, the maximum right value of all regions at or below i. + * @param minLeft At each index i, the minimum left value of all regions at or below i. + */ + public static void fillMaxMinArrays(NodeRegion[] regions, float[] maxRight, float[] minLeft) { float last = 0; for (int i = 0; i < regions.length; i++) { last = Math.max(last, regions[i].mRight); - maxBottom[i] = last; + maxRight[i] = last; } for (int i = regions.length - 1; i >= 0; i--) { last = Math.min(last, regions[i].mLeft); - minTop[i] = last; + minLeft[i] = last; } } + /** + * Populates the max and min arrays for a given set of draw commands. Also populates a mapping of + * react tags to their index position in the command array. + * + * This should never be called from the UI thread, as the reason it exists is to do work off the + * UI thread. + * + * @param commands The draw commands that will eventually be mounted. + * @param maxRight At each index i, the maximum right value of all draw commands at or below i. + * @param minLeft At each index i, the minimum left value of all draw commands at or below i. + * @param drawViewIndexMap Mapping of ids to index position within the draw command array. + */ public static void fillMaxMinArrays( DrawCommand[] commands, - float[] maxBottom, - float[] minTop, + float[] maxRight, + float[] minLeft, SparseIntArray drawViewIndexMap) { float last = 0; // Loop through the DrawCommands, keeping track of the maximum we've seen if we only iterated @@ -88,7 +108,7 @@ import android.util.SparseIntArray; } else { last = Math.max(last, commands[i].getRight()); } - maxBottom[i] = last; + maxRight[i] = last; } // Intentionally leave last as it was, since it's at the maximum bottom position we've seen so // far, we can use it again. @@ -100,7 +120,7 @@ import android.util.SparseIntArray; } else { last = Math.min(last, commands[i].getLeft()); } - minTop[i] = last; + minLeft[i] = last; } } } 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 3b5a6f03b9..f7595fd275 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java @@ -26,17 +26,17 @@ import com.facebook.react.uimanager.events.EventDispatcher; /** * Shadow node hierarchy by itself cannot display UI, it is only a representation of what UI should - * be from JavaScript perspective. StateBuilder is a helper class that can walk the shadow node tree - * and collect information that can then be passed to UI thread and applied to a hierarchy of Views - * that Android finally can display. + * be from JavaScript perspective. StateBuilder is a helper class that walks the shadow node tree + * and collects information into an operation queue that is run on the UI thread and applied to the + * non-shadow hierarchy of Views that Android can finally display. */ /* package */ final class StateBuilder { /* package */ static final float[] EMPTY_FLOAT_ARRAY = new float[0]; - /* package */ static final SparseArray EMPTY_SPARSE_DRAWVIEW = new SparseArray<>(); /* package */ static final SparseIntArray EMPTY_SPARSE_INT = new SparseIntArray(); private static final boolean SKIP_UP_TO_DATE_NODES = true; + // Optimization to avoid re-allocating zero length arrays. private static final int[] EMPTY_INT_ARRAY = new int[0]; private final FlatUIViewOperationQueue mOperationsQueue; @@ -70,8 +70,9 @@ import com.facebook.react.uimanager.events.EventDispatcher; } /** - * Given a root of the laid-out shadow node hierarchy, walks the tree and generates an array of - * DrawCommands that will then mount in UI thread to a root FlatViewGroup so that it can draw. + * Given a root of the laid-out shadow node hierarchy, walks the tree and generates arrays from + * element lists that are mounted in the UI thread to FlatViewGroups to handle drawing, touch, + * and other logic. */ /* package */ void applyUpdates(FlatShadowNode node) { float width = node.getLayoutWidth(); @@ -95,6 +96,13 @@ import com.facebook.react.uimanager.events.EventDispatcher; updateViewBounds(node, left, top, right, bottom); } + /** + * Run after the shadow node hierarchy is updated. Detaches all children from Views that are + * changing their native children, updates views, and dispatches commands before discarding any + * dropped views. + * + * @param eventDispatcher Dispatcher for onLayout events. + */ void afterUpdateViewHierarchy(EventDispatcher eventDispatcher) { if (mDetachAllChildrenFromViews != null) { int[] viewsToDetachAllChildrenFrom = collectViewTags(mViewsToDetachAllChildrenFrom); @@ -109,7 +117,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; } mUpdateViewBoundsOperations.clear(); - // Process view manager commands after bounds operations, so that any ui operations have already + // Process view manager commands after bounds operations, so that any UI operations have already // happened before we actually dispatch the view manager command. This prevents things like // commands going to empty parents and views not yet being created. for (int i = 0, size = mViewManagerCommands.size(); i != size; i++) { @@ -143,16 +151,33 @@ import com.facebook.react.uimanager.events.EventDispatcher; } /** - * Adds a DrawCommand for current mountable node. + * Adds a draw command to the element list for the current scope. Allows collectState within the + * shadow node to add commands. + * + * @param drawCommand The draw command to add. */ /* package */ void addDrawCommand(AbstractDrawCommand drawCommand) { mDrawCommands.add(drawCommand); } + /** + * Adds a listener to the element list for the current scope. Allows collectState within the + * shadow node to add listeners. + * + * @param listener The listener to add + */ /* package */ void addAttachDetachListener(AttachDetachListener listener) { mAttachDetachListeners.add(listener); } + /** + * Adds a command for a view manager to the queue. We have to delay adding it to the operations + * queue until we have added our view moves, creations and updates. + * + * @param reactTag The react tag of the command target. + * @param commandId ID of the command. + * @param commandArgs Arguments for the command. + */ /* package */ void enqueueViewManagerCommand( int reactTag, int commandId, @@ -161,11 +186,17 @@ import com.facebook.react.uimanager.events.EventDispatcher; mOperationsQueue.createViewManagerCommand(reactTag, commandId, commandArgs)); } + /** + * Create a backing view for a node, or update the backing view if it has already been created. + * + * @param node The node to create the backing view for. + * @param styles Styles for the view. + */ /* package */ void enqueueCreateOrUpdateView( FlatShadowNode node, @Nullable ReactStylesDiffMap styles) { if (node.isBackingViewCreated()) { - // if the View is already created, make sure propagate new styles. + // If the View is already created, make sure to propagate the new styles. mOperationsQueue.enqueueUpdateProperties( node.getReactTag(), node.getViewClass(), @@ -181,6 +212,11 @@ import com.facebook.react.uimanager.events.EventDispatcher; } } + /** + * Create a backing view for a node if not already created. + * + * @param node The node to create the backing view for. + */ /* package */ void ensureBackingViewIsCreated(FlatShadowNode node) { if (node.isBackingViewCreated()) { return; @@ -192,10 +228,28 @@ import com.facebook.react.uimanager.events.EventDispatcher; node.signalBackingViewIsCreated(); } + /** + * Enqueue dropping of the view for a node that has a backing view. Used in conjuction with + * remove the node from the shadow hierarchy. + * + * @param node The node to drop the backing view for. + */ /* package */ void dropView(FlatShadowNode node) { mViewsToDrop.add(node.getReactTag()); } + /** + * Adds a node region to the element list for the current scope. Allows collectState to add + * regions. + * + * @param node The node to add a region for. + * @param left Bound of the region. + * @param top Bound of the region. + * @param right Bound of the region. + * @param bottom Bound of the region. + * @param isVirtual True if the region does not map to a native view. Used to determine touch + * targets. + */ private void addNodeRegion( FlatShadowNode node, float left, @@ -212,6 +266,12 @@ import com.facebook.react.uimanager.events.EventDispatcher; mNodeRegions.add(node.getNodeRegion()); } + /** + * Adds a native child to the element list for the current scope. Allows collectState to add + * native children. + * + * @param nativeChild The view-backed native child to add. + */ private void addNativeChild(FlatShadowNode nativeChild) { mNativeChildren.add(nativeChild); } @@ -244,8 +304,9 @@ import com.facebook.react.uimanager.events.EventDispatcher; } /** - * Collects state (DrawCommands) for a given node that will mount to a View. - * Returns true if this node or any of its descendants that mount to View generated any updates. + * Collects state (Draw commands, listeners, regions, native children) for a given node that will + * mount to a View. Returns true if this node or any of its descendants that mount to View + * generated any updates. */ private boolean collectStateForMountableNode( FlatShadowNode node, @@ -413,6 +474,14 @@ import com.facebook.react.uimanager.events.EventDispatcher; return updated; } + /** + * Handles updating the children of a node when they change. Updates the shadow node and + * enqueues state updates that will eventually be run on the UI thread. + * + * @param node The node to update native children for. + * @param oldNativeChildren The previously mounted native children. + * @param newNativeChildren The newly mounted native children. + */ private void updateNativeChildren( FlatShadowNode node, FlatShadowNode[] oldNativeChildren, @@ -470,7 +539,9 @@ import com.facebook.react.uimanager.events.EventDispatcher; } /** - * Recursively walks node tree from a given node and collects DrawCommands. + * Recursively walks node tree from a given node and collects draw commands, listeners, node + * regions and native children. Calls collect state on the node, then processNodeAndCollectState + * for the recursion. */ private boolean collectStateRecursively( FlatShadowNode node, @@ -548,6 +619,11 @@ import com.facebook.react.uimanager.events.EventDispatcher; return updated; } + /** + * Recursively walks this node and child nodes, marking the layout state as UP_TO_DATE. + * + * @param node The node to recur down from. + */ private void markLayoutSeenRecursively(ReactShadowNode node) { if (node.hasNewLayout()) { node.markLayoutSeen(); @@ -559,8 +635,8 @@ import com.facebook.react.uimanager.events.EventDispatcher; } /** - * Collects state and updates View boundaries for a given node tree. - * Returns true if this node or any of its descendants that mount to View generated any updates. + * Collects state and enqueues View boundary updates for a given node tree. Returns true if + * this node or any of its descendants that mount to View generated any updates. */ private boolean processNodeAndCollectState( FlatShadowNode node, diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java index 8d882242c2..c2e08ed9ee 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java @@ -57,13 +57,21 @@ import android.util.SparseIntArray; return mRegionMaxBottom[index] < touchY; } - // These should never be called from the UI thread, as the reason they exist is to do work off - // the UI thread. - public static void fillMaxMinArrays(NodeRegion[] regions, float[] maxBot, float[] minTop) { + /** + * Populates the max and min arrays for a given set of node regions. + * + * This should never be called from the UI thread, as the reason it exists is to do work off the + * UI thread. + * + * @param regions The regions that will eventually be mounted. + * @param maxBottom At each index i, the maximum bottom value of all regions at or below i. + * @param minTop At each index i, the minimum top value of all regions at or below i. + */ + public static void fillMaxMinArrays(NodeRegion[] regions, float[] maxBottom, float[] minTop) { float last = 0; for (int i = 0; i < regions.length; i++) { last = Math.max(last, regions[i].mBottom); - maxBot[i] = last; + maxBottom[i] = last; } for (int i = regions.length - 1; i >= 0; i--) { last = Math.min(last, regions[i].mTop); @@ -71,11 +79,21 @@ import android.util.SparseIntArray; } } - // These should never be called from the UI thread, as the reason they exist is to do work off - // the UI thread. + /** + * Populates the max and min arrays for a given set of draw commands. Also populates a mapping of + * react tags to their index position in the command array. + * + * This should never be called from the UI thread, as the reason it exists is to do work off the + * UI thread. + * + * @param commands The draw commands that will eventually be mounted. + * @param maxBottom At each index i, the maximum bottom value of all draw commands at or below i. + * @param minTop At each index i, the minimum top value of all draw commands at or below i. + * @param drawViewIndexMap Mapping of ids to index position within the draw command array. + */ public static void fillMaxMinArrays( DrawCommand[] commands, - float[] maxBot, + float[] maxBottom, float[] minTop, SparseIntArray drawViewIndexMap) { float last = 0; @@ -90,7 +108,7 @@ import android.util.SparseIntArray; } else { last = Math.max(last, commands[i].getBottom()); } - maxBot[i] = last; + maxBottom[i] = last; } // Intentionally leave last as it was, since it's at the maximum bottom position we've seen so // far, we can use it again.