/**
* 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.views.view;
import javax.annotation.Nullable;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.touch.CatalystInterceptingViewGroup;
import com.facebook.react.touch.OnInterceptTouchEventListener;
import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactPointerEventsView;
/**
* Backing for a React View. Has support for borders, but since borders aren't common, lazy
* initializes most of the storage needed for them.
*/
public class ReactViewGroup extends ViewGroup implements
CatalystInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView {
private static final int ARRAY_CAPACITY_INCREMENT = 12;
private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0);
/* should only be used in {@link #updateClippingToRect} */
private static final Rect sHelperRect = new Rect();
/**
* This listener will be set for child views when removeClippedSubview property is enabled. When
* children layout is updated, it will call {@link #updateSubviewClipStatus} to notify parent
* view about that fact so that view can be attached/detached if necessary.
*
* TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children
* update their layout.
*/
private static final class ChildrenLayoutChangeListener implements OnLayoutChangeListener {
private final ReactViewGroup mParent;
private ChildrenLayoutChangeListener(ReactViewGroup parent) {
mParent = parent;
}
@Override
public void onLayoutChange(
View v,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
if (mParent.getRemoveClippedSubviews()) {
mParent.updateSubviewClipStatus(v);
}
}
}
// Following properties are here to support the option {@code removeClippedSubviews}. This is a
// temporary optimization/hack that is mainly applicable to the large list of images. The way
// it's implemented is that we store an additional array of children in view node. We selectively
// remove some of the views (detach) from it while still storing them in that additional array.
// We override all possible add methods for {@link ViewGroup} so that we can controll this process
// whenever the option is set. We also override {@link ViewGroup#getChildAt} and
// {@link ViewGroup#getChildCount} so those methods may return views that are not attached.
// This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}.
private boolean mRemoveClippedSubviews = false;
private @Nullable View[] mAllChildren = null;
private int mAllChildrenCount;
private @Nullable Rect mClippingRect;
private PointerEvents mPointerEvents = PointerEvents.AUTO;
private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener;
private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
private boolean mNeedsOffscreenAlphaCompositing = false;
public ReactViewGroup(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// No-op since UIManagerModule handles actually laying out children.
}
@Override
public void requestLayout() {
// No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and
// `layout` is called on all RN-managed views by `NativeViewHierarchyManager`
}
@Override
public void setBackgroundColor(int color) {
if (color == Color.TRANSPARENT) {
Drawable backgroundDrawble = getBackground();
if (mReactBackgroundDrawable != null && (backgroundDrawble instanceof LayerDrawable)) {
// extract translucent background portion from layerdrawable
super.setBackground(null);
LayerDrawable layerDrawable = (LayerDrawable) backgroundDrawble;
super.setBackground(layerDrawable.getDrawable(1));
} else if (backgroundDrawble instanceof ReactViewBackgroundDrawable) {
// mReactBackground is set for background
mReactBackgroundDrawable = null;
super.setBackground(null);
}
} else {
getOrCreateReactViewBackground().setColor(color);
}
}
@Override
public void setBackground(Drawable drawable) {
throw new UnsupportedOperationException(
"This method is not supported for ReactViewGroup instances");
}
public void setTranslucentBackgroundDrawable(@Nullable Drawable background) {
// it's required to call setBackground to null, as in some of the cases we may set new
// background to be a layer drawable that contains a drawable that has been previously setup
// as a background previously. This will not work correctly as the drawable callback logic is
// messed up in AOSP
super.setBackground(null);
if (mReactBackgroundDrawable != null && background != null) {
LayerDrawable layerDrawable =
new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, background});
super.setBackground(layerDrawable);
} else if (background != null) {
super.setBackground(background);
}
}
@Override
public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) {
mOnInterceptTouchEventListener = listener;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mOnInterceptTouchEventListener != null &&
mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) {
return true;
}
// We intercept the touch event if the children are not supposed to receive it.
if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_ONLY) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// We do not accept the touch event if this view is not supposed to receive it.
if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_NONE) {
return false;
}
// The root view always assumes any view that was tapped wants the touch
// and sends the event to JS as such.
// We don't need to do bubbling in native (it's already happening in JS).
// For an explanation of bubbling and capturing, see
// http://javascript.info/tutorial/bubbling-and-capturing#capturing
return true;
}
/**
* We override this to allow developers to determine whether they need offscreen alpha compositing
* or not. See the documentation of needsOffscreenAlphaCompositing in View.js.
*/
@Override
public boolean hasOverlappingRendering() {
return mNeedsOffscreenAlphaCompositing;
}
/**
* See the documentation of needsOffscreenAlphaCompositing in View.js.
*/
public void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) {
mNeedsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing;
}
public void setBorderWidth(int position, float width) {
getOrCreateReactViewBackground().setBorderWidth(position, width);
}
public void setBorderColor(int position, float color) {
getOrCreateReactViewBackground().setBorderColor(position, color);
}
public void setBorderRadius(float borderRadius) {
getOrCreateReactViewBackground().setRadius(borderRadius);
}
public void setBorderStyle(@Nullable String style) {
getOrCreateReactViewBackground().setBorderStyle(style);
}
@Override
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
if (removeClippedSubviews == mRemoveClippedSubviews) {
return;
}
mRemoveClippedSubviews = removeClippedSubviews;
if (removeClippedSubviews) {
mClippingRect = new Rect();
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
mAllChildrenCount = getChildCount();
int initialSize = Math.max(12, mAllChildrenCount);
mAllChildren = new View[initialSize];
mChildrenLayoutChangeListener = new ChildrenLayoutChangeListener(this);
for (int i = 0; i < mAllChildrenCount; i++) {
View child = getChildAt(i);
mAllChildren[i] = child;
child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
}
updateClippingRect();
} else {
// Add all clipped views back, deallocate additional arrays, remove layoutChangeListener
Assertions.assertNotNull(mClippingRect);
Assertions.assertNotNull(mAllChildren);
Assertions.assertNotNull(mChildrenLayoutChangeListener);
for (int i = 0; i < mAllChildrenCount; i++) {
mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
}
getDrawingRect(mClippingRect);
updateClippingToRect(mClippingRect);
mAllChildren = null;
mClippingRect = null;
mAllChildrenCount = 0;
mChildrenLayoutChangeListener = null;
}
}
@Override
public boolean getRemoveClippedSubviews() {
return mRemoveClippedSubviews;
}
@Override
public void getClippingRect(Rect outClippingRect) {
outClippingRect.set(mClippingRect);
}
@Override
public void updateClippingRect() {
if (!mRemoveClippedSubviews) {
return;
}
Assertions.assertNotNull(mClippingRect);
Assertions.assertNotNull(mAllChildren);
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
updateClippingToRect(mClippingRect);
}
private void updateClippingToRect(Rect clippingRect) {
Assertions.assertNotNull(mAllChildren);
int clippedSoFar = 0;
for (int i = 0; i < mAllChildrenCount; i++) {
updateSubviewClipStatus(clippingRect, i, clippedSoFar);
if (mAllChildren[i].getParent() == null) {
clippedSoFar++;
}
}
}
private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) {
View child = Assertions.assertNotNull(mAllChildren)[idx];
sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
boolean intersects = clippingRect
.intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom);
boolean needUpdateClippingRecursive = false;
if (!intersects && child.getParent() != null) {
// We can try saving on invalidate call here as the view that we remove is out of visible area
// therefore invalidation is not necessary.
super.removeViewsInLayout(idx - clippedSoFar, 1);
needUpdateClippingRecursive = true;
} else if (intersects && child.getParent() == null) {
super.addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true);
invalidate();
needUpdateClippingRecursive = true;
} else if (intersects && !clippingRect.contains(sHelperRect)) {
// View is partially clipped.
needUpdateClippingRecursive = true;
}
if (needUpdateClippingRecursive) {
if (child instanceof ReactClippingViewGroup) {
// we don't use {@link sHelperRect} until the end of this loop, therefore it's safe
// to call this method that may write to the same {@link sHelperRect} object.
ReactClippingViewGroup clippingChild = (ReactClippingViewGroup) child;
if (clippingChild.getRemoveClippedSubviews()) {
clippingChild.updateClippingRect();
}
}
}
}
private void updateSubviewClipStatus(View subview) {
if (!mRemoveClippedSubviews || getParent() == null) {
return;
}
Assertions.assertNotNull(mClippingRect);
Assertions.assertNotNull(mAllChildren);
// do fast check whether intersect state changed
sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom());
boolean intersects = mClippingRect
.intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom);
// If it was intersecting before, should be attached to the parent
boolean oldIntersects = (subview.getParent() != null);
if (intersects != oldIntersects) {
int clippedSoFar = 0;
for (int i = 0; i < mAllChildrenCount; i++) {
if (mAllChildren[i] == subview) {
updateSubviewClipStatus(mClippingRect, i, clippedSoFar);
break;
}
if (mAllChildren[i].getParent() == null) {
clippedSoFar++;
}
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateClippingRect();
}
@Override
public PointerEvents getPointerEvents() {
return mPointerEvents;
}
/*package*/ void setPointerEvents(PointerEvents pointerEvents) {
mPointerEvents = pointerEvents;
}
/*package*/ int getAllChildrenCount() {
return mAllChildrenCount;
}
/*package*/ View getChildAtWithSubviewClippingEnabled(int index) {
return Assertions.assertNotNull(mAllChildren)[index];
}
/*package*/ void addViewWithSubviewClippingEnabled(View child, int index) {
addViewWithSubviewClippingEnabled(child, index, sDefaultLayoutParam);
}
/*package*/ void addViewWithSubviewClippingEnabled(View child, int index, LayoutParams params) {
Assertions.assertCondition(mRemoveClippedSubviews);
Assertions.assertNotNull(mClippingRect);
Assertions.assertNotNull(mAllChildren);
addInArray(child, index);
// we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally
// attach it
int clippedSoFar = 0;
for (int i = 0; i < index; i++) {
if (mAllChildren[i].getParent() == null) {
clippedSoFar++;
}
}
updateSubviewClipStatus(mClippingRect, index, clippedSoFar);
child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
}
/*package*/ void removeViewWithSubviewClippingEnabled(View view) {
Assertions.assertCondition(mRemoveClippedSubviews);
Assertions.assertNotNull(mClippingRect);
Assertions.assertNotNull(mAllChildren);
view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
int index = indexOfChildInAllChildren(view);
if (mAllChildren[index].getParent() != null) {
int clippedSoFar = 0;
for (int i = 0; i < index; i++) {
if (mAllChildren[i].getParent() == null) {
clippedSoFar++;
}
}
super.removeViewsInLayout(index - clippedSoFar, 1);
}
removeFromArray(index);
}
private int indexOfChildInAllChildren(View child) {
final int count = mAllChildrenCount;
final View[] children = Assertions.assertNotNull(mAllChildren);
for (int i = 0; i < count; i++) {
if (children[i] == child) {
return i;
}
}
return -1;
}
private void addInArray(View child, int index) {
View[] children = Assertions.assertNotNull(mAllChildren);
final int count = mAllChildrenCount;
final int size = children.length;
if (index == count) {
if (size == count) {
mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
System.arraycopy(children, 0, mAllChildren, 0, size);
children = mAllChildren;
}
children[mAllChildrenCount++] = child;
} else if (index < count) {
if (size == count) {
mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
System.arraycopy(children, 0, mAllChildren, 0, index);
System.arraycopy(children, index, mAllChildren, index + 1, count - index);
children = mAllChildren;
} else {
System.arraycopy(children, index, children, index + 1, count - index);
}
children[index] = child;
mAllChildrenCount++;
} else {
throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
}
}
// This method also sets the child's mParent to null
private void removeFromArray(int index) {
final View[] children = Assertions.assertNotNull(mAllChildren);
final int count = mAllChildrenCount;
if (index == count - 1) {
children[--mAllChildrenCount] = null;
} else if (index >= 0 && index < count) {
System.arraycopy(children, index + 1, children, index, count - index - 1);
children[--mAllChildrenCount] = null;
} else {
throw new IndexOutOfBoundsException();
}
}
@VisibleForTesting
public int getBackgroundColor() {
if (getBackground() != null) {
return ((ReactViewBackgroundDrawable) getBackground()).getColor();
}
return DEFAULT_BACKGROUND_COLOR;
}
private ReactViewBackgroundDrawable getOrCreateReactViewBackground() {
if (mReactBackgroundDrawable == null) {
mReactBackgroundDrawable = new ReactViewBackgroundDrawable();
Drawable backgroundDrawable = getBackground();
super.setBackground(null); // required so that drawable callback is cleared before we add the
// drawable back as a part of LayerDrawable
if (backgroundDrawable == null) {
super.setBackground(mReactBackgroundDrawable);
} else {
LayerDrawable layerDrawable =
new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, backgroundDrawable});
super.setBackground(layerDrawable);
}
}
return mReactBackgroundDrawable;
}
}