Files
react-native/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java
T
Denis Koroskin 529390b87c Fix children of AndroidView not being re-laid out after they call requestLayout
Summary:
In ReactNative, we are fully controlling layout of all the Views, not allowing Android to layout anything for us. This is done by making onLayout() of the top-level View in the hierarchy to be empty. This works fine because we explicitly call measure/layout for all the Views when they need to be re-measured or re-laid out. There is however one case where this doesn't happen automatically: some Android Views such as DrawerLayout or ActionBar have children that don't have shadow nodes associated with them (such as a title in ActionBar). This results in situations where children of AndroidView will call requestLayout but they will never get relaid out, because shadow hierarchy doesn't know about them. Example: ActionBar has a seTitle method that will internally call TextView.setTitle() and that TextView will call requestLayout because its size may have changed. However, that TextView will never be remeasured or relaid out.

This diff is fixing it by keeping track of everyone who called requestLayout. Then, at the end of the update loop we go over the list a manually remeasure and relayout those Views.

Not a huge fan of how this is implemented (there MUST be a better way) but this works with least efforts. I'll see if I can improve it later.

Reviewed By: ahmedre

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

261 lines
7.8 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 java.util.ArrayList;
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 static final ArrayList<FlatViewGroup> LAYOUT_REQUESTS = new ArrayList<>();
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;
private boolean mIsLayoutRequested = false;
/* package */ FlatViewGroup(Context context) {
super(context);
}
@Override
protected void detachAllViewsFromParent() {
super.detachAllViewsFromParent();
}
@Override
public void requestLayout() {
if (mIsLayoutRequested) {
return;
}
mIsLayoutRequested = true;
LAYOUT_REQUESTS.add(this);
}
@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));
addViewInLayout(view, -1, ensureLayoutParams(view.getLayoutParams()), true);
} else {
View view = ensureViewHasNoParent(viewResolver.getView(-viewToAdd));
attachViewToParent(view, -1, ensureLayoutParams(view.getLayoutParams()));
}
}
for (int viewToDetach : viewsToDetach) {
removeDetachedView(viewResolver.getView(viewToDetach), false);
}
}
/* package */ void processLayoutRequest() {
mIsLayoutRequested = false;
for (int i = 0, childCount = getChildCount(); i != childCount; ++i) {
View child = getChildAt(i);
if (!child.isLayoutRequested()) {
continue;
}
child.measure(
MeasureSpec.makeMeasureSpec(child.getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(child.getHeight(), MeasureSpec.EXACTLY));
child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
}
}
/* package */ static void processLayoutRequests() {
for (int i = 0, numLayoutRequests = LAYOUT_REQUESTS.size(); i != numLayoutRequests; ++i) {
FlatViewGroup flatViewGroup = LAYOUT_REQUESTS.get(i);
flatViewGroup.processLayoutRequest();
}
LAYOUT_REQUESTS.clear();
}
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();
}
}