Plumbing to set filters on Android views (#44453)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/44453

This works similar to how `transform` is parsed in that it sets tags on the View to actually update the prop when all the prop setters are done being called since the parsing of the array is not very trivial. Besides that it is pretty simple and just calls into `FilterHelper` and uses `setRenderEffect`: https://developer.android.com/reference/android/view/View#setRenderEffect(android.graphics.RenderEffect).

That API is only exposed in version 31 of the SDK so it is gated accordingly.

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D54640600

fbshipit-source-id: ad4cde2bed9611f476f4ecb2550c2269965d7917
This commit is contained in:
Joe Vilches
2024-05-08 13:39:09 -07:00
committed by Facebook GitHub Bot
parent 6213b2a62a
commit b0e746e4bb
6 changed files with 242 additions and 129 deletions
@@ -3970,6 +3970,7 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo
public fun setClick (Landroid/view/View;Z)V
public fun setClickCapture (Landroid/view/View;Z)V
public fun setElevation (Landroid/view/View;F)V
public fun setFilter (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V
public fun setImportantForAccessibility (Landroid/view/View;Ljava/lang/String;)V
public fun setMoveShouldSetResponder (Landroid/view/View;Z)V
public fun setMoveShouldSetResponderCapture (Landroid/view/View;Z)V
@@ -4039,6 +4040,7 @@ public abstract interface class com/facebook/react/uimanager/BaseViewManagerInte
public abstract fun setBorderTopLeftRadius (Landroid/view/View;F)V
public abstract fun setBorderTopRightRadius (Landroid/view/View;F)V
public abstract fun setElevation (Landroid/view/View;F)V
public abstract fun setFilter (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V
public abstract fun setImportantForAccessibility (Landroid/view/View;Ljava/lang/String;)V
public abstract fun setNativeId (Landroid/view/View;Ljava/lang/String;)V
public abstract fun setOpacity (Landroid/view/View;F)V
@@ -5333,6 +5335,7 @@ public final class com/facebook/react/uimanager/ViewProps {
public static final field ELLIPSIZE_MODE Ljava/lang/String;
public static final field ENABLED Ljava/lang/String;
public static final field END Ljava/lang/String;
public static final field FILTER Ljava/lang/String;
public static final field FLEX Ljava/lang/String;
public static final field FLEX_BASIS Ljava/lang/String;
public static final field FLEX_DIRECTION Ljava/lang/String;
@@ -8,6 +8,7 @@
package com.facebook.react.uimanager;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.text.TextUtils;
import android.view.View;
@@ -104,6 +105,10 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
view.setTag(R.id.invalidate_transform, null);
view.removeOnLayoutChangeListener(this);
view.setTag(R.id.use_hardware_layer, null);
view.setTag(R.id.filter, null);
applyFilter(view, null);
// setShadowColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
view.setOutlineAmbientShadowColor(Color.BLACK);
@@ -181,6 +186,12 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
view.setBackgroundColor(backgroundColor);
}
@Override
@ReactProp(name = ViewProps.FILTER)
public void setFilter(@NonNull T view, @Nullable ReadableArray filter) {
view.setTag(R.id.filter, filter);
}
@Override
@ReactProp(name = ViewProps.TRANSFORM)
public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) {
@@ -235,7 +246,7 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
@Override
@ReactProp(name = ViewProps.RENDER_TO_HARDWARE_TEXTURE)
public void setRenderToHardwareTexture(@NonNull T view, boolean useHWTexture) {
view.setLayerType(useHWTexture ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
view.setTag(R.id.use_hardware_layer, useHWTexture);
}
@Override
@@ -491,6 +502,28 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
}
}
private void applyFilter(@NonNull T view, @Nullable ReadableArray filter) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
view.setRenderEffect(null);
}
Boolean useHWLayer = (Boolean) view.getTag(R.id.use_hardware_layer);
int layerType =
useHWLayer != null && useHWLayer ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE;
view.setLayerType(layerType, null);
if (filter == null) {
return;
}
if (FilterHelper.isOnlyColorMatrixFilters(filter)) {
Paint p = new Paint();
p.setColorFilter(FilterHelper.parseColorMatrixFilters(filter));
view.setLayerType(View.LAYER_TYPE_HARDWARE, p);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
view.setRenderEffect(FilterHelper.parseFilters(filter));
}
}
protected void setTransformProperty(
@NonNull T view,
@Nullable ReadableArray transforms,
@@ -595,6 +628,14 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
setTransformProperty(view, transformMatrix, transformOrigin);
view.setTag(R.id.invalidate_transform, false);
}
Boolean useHWLayer = (Boolean) view.getTag(R.id.use_hardware_layer);
if (useHWLayer != null) {
view.setLayerType(useHWLayer ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
}
ReadableArray filter = (ReadableArray) view.getTag(R.id.filter);
applyFilter(view, filter);
}
@Override
@@ -48,6 +48,8 @@ public interface BaseViewManagerInterface<T extends View> {
void setElevation(T view, float elevation);
void setFilter(T view, ReadableArray filter);
void setShadowColor(T view, int shadowColor);
void setImportantForAccessibility(T view, @Nullable String importantForAccessibility);
@@ -12,8 +12,9 @@ import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.RenderEffect
import android.graphics.Shader
import android.graphics.Shader.TileMode
import com.facebook.react.bridge.ReadableArray
import kotlin.math.cos
import kotlin.math.sin
@TargetApi(31)
internal object FilterHelper {
@@ -23,9 +24,9 @@ internal object FilterHelper {
filters ?: return null
var chainedEffects: RenderEffect? = null
for (i in 0 until filters.size()) {
val filter = filters.getMap(i)
val filterName = filter.getString("name") ?: continue
val amount = filter.getDouble("amount").toFloat()
val filter = filters.getMap(i).getEntryIterator().next()
val filterName = filter.key
val amount = (filter.value as Double).toFloat()
chainedEffects =
when (filterName) {
@@ -43,6 +44,47 @@ internal object FilterHelper {
return chainedEffects
}
@JvmStatic
public fun parseColorMatrixFilters(filters: ReadableArray?): ColorMatrixColorFilter? {
filters ?: return null
// New ColorMatrix objects represent the identity matrix
val resultColorMatrix = ColorMatrix()
for (i in 0 until filters.size()) {
val filter = filters.getMap(i).getEntryIterator().next()
val filterName = filter.key
val amount = (filter.value as Double).toFloat()
val tempColorMatrix =
when (filterName) {
"brightness" -> createBrightnessColorMatrix(amount)
"contrast" -> createContrastColorMatrix(amount)
"grayscale" -> createGrayscaleColorMatrix(amount)
"sepia" -> createSepiaColorMatrix(amount)
"saturate" -> createSaturateColorMatrix(amount)
"hueRotate" -> createHueRotateColorMatrix(amount)
"invert" -> createInvertColorMatrix(amount)
else -> throw IllegalArgumentException("Invalid color matrix filter: $filterName")
}
resultColorMatrix.preConcat(tempColorMatrix)
}
return ColorMatrixColorFilter(resultColorMatrix)
}
@JvmStatic
public fun isOnlyColorMatrixFilters(filters: ReadableArray?): Boolean {
filters ?: return false
for (i in 0 until filters.size()) {
val filter = filters.getMap(i).getEntryIterator().next()
val filterName = filter.key
if (filterName == "blur") {
return false
}
}
return true
}
// https://www.w3.org/TR/filter-effects-1/#blurEquivalent
public fun createBlurEffect(sigma: Float, chainedEffects: RenderEffect? = null): RenderEffect? {
if (sigma <= 0.5) {
@@ -66,9 +108,13 @@ internal object FilterHelper {
amount: Float,
chainedEffects: RenderEffect? = null
): RenderEffect {
return createColorMatrixEffect(createBrightnessColorMatrix(amount), chainedEffects)
}
private fun createBrightnessColorMatrix(amount: Float): ColorMatrix {
val matrix = ColorMatrix()
matrix.setScale(amount, amount, amount, 1f)
return createColorMatrixEffect(matrix, chainedEffects)
return matrix
}
// https://www.w3.org/TR/filter-effects-1/#contrastEquivalent
@@ -76,33 +122,35 @@ internal object FilterHelper {
amount: Float,
chainedEffects: RenderEffect? = null
): RenderEffect {
return createColorMatrixEffect(createContrastColorMatrix(amount), chainedEffects)
}
private fun createContrastColorMatrix(amount: Float): ColorMatrix {
// Multiply by 255 as Android operates in [0, 255] while the spec operates in [0, 1].
// This really only matters if there is an intercept that needs to be added
val intercept = 255 * (-(amount / 2.0f) + 0.5f)
val matrix =
ColorMatrix(
floatArrayOf(
amount,
0f,
0f,
0f,
intercept,
0f,
amount,
0f,
0f,
intercept,
0f,
0f,
amount,
0f,
intercept,
0f,
0f,
0f,
1f,
0f))
return createColorMatrixEffect(matrix, chainedEffects)
return ColorMatrix(
floatArrayOf(
amount,
0f,
0f,
0f,
intercept,
0f,
amount,
0f,
0f,
intercept,
0f,
0f,
amount,
0f,
intercept,
0f,
0f,
0f,
1f,
0f))
}
// https://www.w3.org/TR/filter-effects-1/#grayscaleEquivalent
@@ -110,60 +158,64 @@ internal object FilterHelper {
amount: Float,
chainedEffects: RenderEffect? = null
): RenderEffect {
return createColorMatrixEffect(createGrayscaleColorMatrix(amount), chainedEffects)
}
private fun createGrayscaleColorMatrix(amount: Float): ColorMatrix {
val inverseAmount = 1 - amount
val matrix =
ColorMatrix(
floatArrayOf(
0.2_126f + 0.7_874f * inverseAmount,
0.7_152f - 0.7_152f * inverseAmount,
0.0_722f - 0.0_722f * inverseAmount,
0f,
0f,
0.2_126f - 0.2_126f * inverseAmount,
0.7_152f + 0.2_848f * inverseAmount,
0.0_722f - 0.0_722f * inverseAmount,
0f,
0f,
0.2_126f - 0.2_126f * inverseAmount,
0.7_152f - 0.7_152f * inverseAmount,
0.0_722f + 0.9_278f * inverseAmount,
0f,
0f,
0f,
0f,
0f,
1f,
0f))
return createColorMatrixEffect(matrix, chainedEffects)
return ColorMatrix(
floatArrayOf(
0.2_126f + 0.7_874f * inverseAmount,
0.7_152f - 0.7_152f * inverseAmount,
0.0_722f - 0.0_722f * inverseAmount,
0f,
0f,
0.2_126f - 0.2_126f * inverseAmount,
0.7_152f + 0.2_848f * inverseAmount,
0.0_722f - 0.0_722f * inverseAmount,
0f,
0f,
0.2_126f - 0.2_126f * inverseAmount,
0.7_152f - 0.7_152f * inverseAmount,
0.0_722f + 0.9_278f * inverseAmount,
0f,
0f,
0f,
0f,
0f,
1f,
0f))
}
// https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent
public fun createSepiaEffect(amount: Float, chainedEffects: RenderEffect? = null): RenderEffect {
return createColorMatrixEffect(createSepiaColorMatrix(amount), chainedEffects)
}
private fun createSepiaColorMatrix(amount: Float): ColorMatrix {
val inverseAmount = 1 - amount
val matrix =
ColorMatrix(
floatArrayOf(
0.393f + 0.607f * inverseAmount,
0.769f - 0.769f * inverseAmount,
0.189f - 0.189f * inverseAmount,
0f,
0f,
0.349f - 0.349f * inverseAmount,
0.686f + 0.314f * inverseAmount,
0.168f - 0.168f * inverseAmount,
0f,
0f,
0.272f - 0.272f * inverseAmount,
0.534f - 0.534f * inverseAmount,
0.131f + 0.869f * inverseAmount,
0f,
0f,
0f,
0f,
0f,
1f,
0f))
return createColorMatrixEffect(matrix, chainedEffects)
return ColorMatrix(
floatArrayOf(
0.393f + 0.607f * inverseAmount,
0.769f - 0.769f * inverseAmount,
0.189f - 0.189f * inverseAmount,
0f,
0f,
0.349f - 0.349f * inverseAmount,
0.686f + 0.314f * inverseAmount,
0.168f - 0.168f * inverseAmount,
0f,
0f,
0.272f - 0.272f * inverseAmount,
0.534f - 0.534f * inverseAmount,
0.131f + 0.869f * inverseAmount,
0f,
0f,
0f,
0f,
0f,
1f,
0f))
}
// https://www.w3.org/TR/filter-effects-1/#saturateEquivalent
@@ -171,9 +223,13 @@ internal object FilterHelper {
amount: Float,
chainedEffects: RenderEffect? = null
): RenderEffect {
return createColorMatrixEffect(createSaturateColorMatrix(amount), chainedEffects)
}
private fun createSaturateColorMatrix(amount: Float): ColorMatrix {
val matrix = ColorMatrix()
matrix.setSaturation(amount)
return createColorMatrixEffect(matrix, chainedEffects)
return matrix
}
// https://www.w3.org/TR/filter-effects-1/#huerotateEquivalent
@@ -181,63 +237,67 @@ internal object FilterHelper {
amount: Float,
chainedEffects: RenderEffect? = null
): RenderEffect {
return createColorMatrixEffect(createHueRotateColorMatrix(amount), chainedEffects)
}
private fun createHueRotateColorMatrix(amount: Float): ColorMatrix {
val amountRads = Math.toRadians(amount.toDouble())
val cos = Math.cos(amountRads).toFloat()
val sin = Math.sin(amountRads).toFloat()
val matrix =
ColorMatrix(
floatArrayOf(
0.213f + 0.787f * cos - 0.213f * sin,
0.715f - 0.715f * cos - 0.715f * sin,
0.072f - 0.072f * cos + 0.928f * sin,
0f,
0f,
0.213f - 0.213f * cos + 0.143f * sin,
0.715f + 0.285f * cos + 0.140f * sin,
0.072f - 0.072f * cos - 0.283f * sin,
0f,
0f,
0.213f - 0.213f * cos - 0.787f * sin,
0.715f - 0.715f * cos + 0.715f * sin,
0.072f + 0.928f * cos + 0.072f * sin,
0f,
0f,
0f,
0f,
0f,
1f,
0f))
return createColorMatrixEffect(matrix, chainedEffects)
val cos = cos(amountRads).toFloat()
val sin = sin(amountRads).toFloat()
return ColorMatrix(
floatArrayOf(
0.213f + 0.787f * cos - 0.213f * sin,
0.715f - 0.715f * cos - 0.715f * sin,
0.072f - 0.072f * cos + 0.928f * sin,
0f,
0f,
0.213f - 0.213f * cos + 0.143f * sin,
0.715f + 0.285f * cos + 0.140f * sin,
0.072f - 0.072f * cos - 0.283f * sin,
0f,
0f,
0.213f - 0.213f * cos - 0.787f * sin,
0.715f - 0.715f * cos + 0.715f * sin,
0.072f + 0.928f * cos + 0.072f * sin,
0f,
0f,
0f,
0f,
0f,
1f,
0f))
}
// https://www.w3.org/TR/filter-effects-1/#invertEquivalent
public fun createInvertEffect(amount: Float, chainedEffects: RenderEffect? = null): RenderEffect {
return createColorMatrixEffect(createInvertColorMatrix(amount), chainedEffects)
}
private fun createInvertColorMatrix(amount: Float): ColorMatrix {
val slope = 1 - 2 * amount
val intercept = amount * 255
val matrix =
ColorMatrix(
floatArrayOf(
slope,
0f,
0f,
0f,
intercept,
0f,
slope,
0f,
0f,
intercept,
0f,
0f,
slope,
0f,
intercept,
0f,
0f,
0f,
1f,
0f))
return createColorMatrixEffect(matrix, chainedEffects)
return ColorMatrix(
floatArrayOf(
slope,
0f,
0f,
0f,
intercept,
0f,
slope,
0f,
0f,
intercept,
0f,
0f,
slope,
0f,
intercept,
0f,
0f,
0f,
1f,
0f))
}
private fun createColorMatrixEffect(
@@ -135,6 +135,7 @@ public object ViewProps {
public const val BORDER_START_COLOR: String = "borderStartColor"
public const val BORDER_END_COLOR: String = "borderEndColor"
public const val ON_LAYOUT: String = "onLayout"
public const val FILTER: String = "experimental_filter"
public const val TRANSFORM: String = "transform"
public const val TRANSFORM_ORIGIN: String = "transformOrigin"
public const val ELEVATION: String = "elevation"
@@ -56,4 +56,10 @@
<!-- tag is used to invalidate transform style in view manager -->
<item type="id" name="invalidate_transform"/>
<!-- tag is used to store if we should render the view to a hardware texture -->
<item type="id" name="use_hardware_layer"/>
<!-- tag is used to store graphical filter effects to apply to the view -->
<item type="id" name="filter"/>
</resources>