/** * 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 { 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(); } }