Ensure elevated views are not changing drawing order

Summary:
Uses `enableZ` trick to ensure that Skia doesn't try to reorder the views based on elevation. Unfortunately, it only helps for some cases, but the proper fix would require reimplementing `ViewGroup` completely to remove its internal logic of reordering based on Z coordinate.

Changelog: [Android][Fixed] Ensure elevated views are behind sticky header in FlatList

Reviewed By: cortinico

Differential Revision: D30700566

fbshipit-source-id: d2c59b22332922c610f4f2d415df34e81f5a33c5
This commit is contained in:
Andrei Shikov
2021-09-24 10:03:36 -07:00
committed by Facebook GitHub Bot
parent 4e8da9b28f
commit 4090195122
3 changed files with 112 additions and 0 deletions
@@ -95,4 +95,6 @@ public class ReactFeatureFlags {
public static boolean enableLockFreeEventDispatcher = false;
public static boolean enableAggressiveEventEmitterCleanup = false;
public static boolean insertZReorderBarriersOnViewGroupChildren = true;
}
@@ -0,0 +1,92 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.view;
import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.os.Build;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.annotation.Nullable;
/**
* Copied from <a
* href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/CanvasUtils.android.kt;drc=3b2dde134afab8d58b9c39ad4820eaf9a6e014a9">
* Compose canvas utils </a>
*/
public class CanvasUtil {
private CanvasUtil() {}
private @Nullable static Method mReorderBarrierMethod = null;
private @Nullable static Method mInorderBarrierMethod = null;
private static boolean mOrderMethodsFetched = false;
/**
* Enables Z support for the Canvas. The method is publicly available starting from API 29 and was
* hidden before, so we have to resort to reflection tricks to ensure we can use this API.
*/
@SuppressLint({"SoonBlockedPrivateApi", "PrivateApi"})
public static void enableZ(Canvas canvas, boolean enable) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
if (Build.VERSION.SDK_INT >= 29) {
if (enable) {
canvas.enableZ();
} else {
canvas.disableZ();
}
} else {
fetchOrderMethods();
try {
if (enable && mReorderBarrierMethod != null) {
mReorderBarrierMethod.invoke(canvas);
}
if (!enable && mInorderBarrierMethod != null) {
mInorderBarrierMethod.invoke(canvas);
}
} catch (IllegalAccessException | InvocationTargetException ignore) {
// Do nothing
}
}
}
private static void fetchOrderMethods() {
if (!mOrderMethodsFetched) {
try {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
// use double reflection to avoid grey list on P
Method getDeclaredMethod =
Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
mReorderBarrierMethod =
(Method) getDeclaredMethod.invoke(Canvas.class, "insertReorderBarrier", new Class[0]);
mInorderBarrierMethod =
(Method) getDeclaredMethod.invoke(Canvas.class, "insertInorderBarrier", new Class[0]);
} else {
mReorderBarrierMethod = Canvas.class.getDeclaredMethod("insertReorderBarrier");
mInorderBarrierMethod = Canvas.class.getDeclaredMethod("insertInorderBarrier");
}
if (mReorderBarrierMethod == null || mInorderBarrierMethod == null) {
return;
}
mReorderBarrierMethod.setAccessible(true);
mInorderBarrierMethod.setAccessible(true);
} catch (IllegalAccessException ignore) {
// Do nothing
} catch (InvocationTargetException ignore) {
// Do nothing
} catch (NoSuchMethodException ignore) {
// Do nothing
}
mOrderMethodsFetched = true;
}
}
}
@@ -33,6 +33,7 @@ import com.facebook.react.bridge.ReactNoCrashSoftException;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.modules.i18nmanager.I18nUtil;
import com.facebook.react.touch.OnInterceptTouchEventListener;
import com.facebook.react.touch.ReactHitSlopView;
@@ -759,6 +760,23 @@ public class ReactViewGroup extends ViewGroup
}
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean drawWithZ =
child.getElevation() > 0 && ReactFeatureFlags.insertZReorderBarriersOnViewGroupChildren;
if (drawWithZ) {
CanvasUtil.enableZ(canvas, false);
}
boolean result = super.drawChild(canvas, child, drawingTime);
if (drawWithZ) {
CanvasUtil.enableZ(canvas, true);
}
return result;
}
private void dispatchOverflowDraw(Canvas canvas) {
if (mOverflow != null) {
switch (mOverflow) {