Add comments to FlatViewGroup, DrawCommandManage, ElementsList and StateBuilder.

Summary: Documentate all of the things.

Reviewed By: ahmedre

Differential Revision: D3700420
This commit is contained in:
Seth Kirby 2016-08-15 14:16:53 -07:00 committed by Ahmed El-Helw
parent 3adbf1e822
commit 8600723402
8 changed files with 656 additions and 74 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<FlatViewGroup> {
@ -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<FlatViewGroup> 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<View> 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<View> 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();

View File

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

View File

@ -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<DrawView> 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,

View File

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