Files
react-native/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java
Denis Koroskin dad378e394 Add NodeRegion to allow any FlatShadowNode to respond to touch events
Summary: @public When Android dispatches `MotionEvent` to `ReactRootView`, it needs to find a correspoding react node that should receive it. To be able to do it, we need to store boundaries of every `FlatShadowNode` in `FlatViewGroup`. Then we can iterate over node boundaries and find one that contains the touch event coordinates.

Reviewed By: sriramramani

Differential Revision: D2694197
2016-12-19 13:40:16 -08:00

224 lines
6.7 KiB
Java

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import java.lang.ref.WeakReference;
import javax.annotation.Nullable;
import android.content.Context;
import android.graphics.Canvas;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.facebook.react.uimanager.ReactCompoundView;
/**
* A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over
* array of DrawCommands, executing them one by one.
*/
/* package */ final class FlatViewGroup extends ViewGroup implements ReactCompoundView {
/**
* Helper class that allows AttachDetachListener to invalidate the hosting View.
*/
static final class InvalidateCallback extends WeakReference<FlatViewGroup> {
private InvalidateCallback(FlatViewGroup view) {
super(view);
}
/**
* Propagates invalidate() call up to the hosting View (if it's still alive)
*/
public void invalidate() {
FlatViewGroup view = get();
if (view != null) {
view.invalidate();
}
}
}
private @Nullable InvalidateCallback mInvalidateCallback;
private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY;
private NodeRegion[] mNodeRegions = NodeRegion.EMPTY_ARRAY;
private int mDrawChildIndex = 0;
private boolean mIsAttached = false;
/* package */ FlatViewGroup(Context context) {
super(context);
}
@Override
protected void detachAllViewsFromParent() {
super.detachAllViewsFromParent();
}
@Override
public int reactTagForTouch(float touchX, float touchY) {
for (NodeRegion nodeRegion : mNodeRegions) {
if (nodeRegion.mLeft <= touchX && touchX < nodeRegion.mRight &&
nodeRegion.mTop <= touchY && touchY < nodeRegion.mBottom) {
return nodeRegion.mTag;
}
}
// no children found
return getId();
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
for (DrawCommand drawCommand : mDrawCommands) {
drawCommand.draw(this, canvas);
}
if (mDrawChildIndex != getChildCount()) {
throw new RuntimeException(
"Did not draw all children: " + mDrawChildIndex + " / " + getChildCount());
}
mDrawChildIndex = 0;
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
// suppress
// no drawing -> no invalidate -> return false
return false;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// nothing to do here
}
@Override
protected void onAttachedToWindow() {
if (mIsAttached) {
// this is possible, unfortunately.
return;
}
mIsAttached = true;
super.onAttachedToWindow();
dispatchOnAttached(mAttachDetachListeners);
}
@Override
protected void onDetachedFromWindow() {
if (!mIsAttached) {
throw new RuntimeException("Double detach");
}
mIsAttached = false;
super.onDetachedFromWindow();
dispatchOnDetached(mAttachDetachListeners);
}
/* package */ void drawNextChild(Canvas canvas) {
View child = getChildAt(mDrawChildIndex);
super.drawChild(canvas, child, getDrawingTime());
++mDrawChildIndex;
}
/* package */ void mountDrawCommands(DrawCommand[] drawCommands) {
mDrawCommands = drawCommands;
invalidate();
}
/* package */ void mountAttachDetachListeners(AttachDetachListener[] listeners) {
if (mIsAttached) {
// Ordering of the following 2 statements is very important. While logically it makes sense to
// detach old listeners first, and only then attach new listeners, this is not very efficient,
// because a listener can be in both lists. In this case, it will be detached first and then
// re-attached immediately. This is undesirable for a couple of reasons:
// 1) performance. Detaching is slow because it may cancel an ongoing network request
// 2) it may cause flicker: an image that was already loaded may get unloaded.
//
// For this reason, we are attaching new listeners first. What this means is that listeners
// that are in both lists need to gracefully handle a secondary attach and detach events,
// (i.e. onAttach() being called when already attached, followed by a detach that should be
// ignored) turning them into no-ops. This will result in no performance loss and no flicker,
// because ongoing network requests don't get cancelled.
dispatchOnAttached(listeners);
dispatchOnDetached(mAttachDetachListeners);
}
mAttachDetachListeners = listeners;
}
/* package */ void mountNodeRegions(NodeRegion[] nodeRegions) {
mNodeRegions = nodeRegions;
}
/* package */ void mountViews(ViewResolver viewResolver, int[] viewsToAdd, int[] viewsToDetach) {
for (int viewToAdd : viewsToAdd) {
if (viewToAdd > 0) {
View view = ensureViewHasNoParent(viewResolver.getView(viewToAdd));
addView(view, -1, ensureLayoutParams(view.getLayoutParams()));
} else {
View view = ensureViewHasNoParent(viewResolver.getView(-viewToAdd));
attachViewToParent(view, -1, ensureLayoutParams(view.getLayoutParams()));
}
}
for (int viewToDetach : viewsToDetach) {
removeDetachedView(viewResolver.getView(viewToDetach), false);
}
}
private View ensureViewHasNoParent(View view) {
ViewParent oldParent = view.getParent();
if (oldParent != null) {
throw new RuntimeException(
"Cannot add view " + view + " to " + this + " while it has a parent " + oldParent);
}
return view;
}
private void dispatchOnAttached(AttachDetachListener[] listeners) {
int numListeners = listeners.length;
if (numListeners == 0) {
return;
}
InvalidateCallback callback = getInvalidateCallback();
for (AttachDetachListener listener : listeners) {
listener.onAttached(callback);
}
}
private InvalidateCallback getInvalidateCallback() {
if (mInvalidateCallback == null) {
mInvalidateCallback = new InvalidateCallback(this);
}
return mInvalidateCallback;
}
private static void dispatchOnDetached(AttachDetachListener[] listeners) {
for (AttachDetachListener listener : listeners) {
listener.onDetached();
}
}
private ViewGroup.LayoutParams ensureLayoutParams(ViewGroup.LayoutParams lp) {
if (checkLayoutParams(lp)) {
return lp;
}
return generateDefaultLayoutParams();
}
}