From b0e746e4bb3a27d3aee33d9228e62c8d2e336aef Mon Sep 17 00:00:00 2001 From: Joe Vilches Date: Wed, 8 May 2024 13:39:09 -0700 Subject: [PATCH] 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 --- .../ReactAndroid/api/ReactAndroid.api | 3 + .../react/uimanager/BaseViewManager.java | 43 ++- .../uimanager/BaseViewManagerInterface.java | 2 + .../facebook/react/uimanager/FilterHelper.kt | 316 +++++++++++------- .../com/facebook/react/uimanager/ViewProps.kt | 1 + .../main/res/views/uimanager/values/ids.xml | 6 + 6 files changed, 242 insertions(+), 129 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 929b851ac51..f2e69724317 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 8b8322e5032..5c978e070d9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -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= Build.VERSION_CODES.P) { view.setOutlineAmbientShadowColor(Color.BLACK); @@ -181,6 +186,12 @@ public abstract class BaseViewManager= 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 { 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); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt index f0fbf919f31..1c59d400ab6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt @@ -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( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt index a976bd13da9..5b6c2bef0d6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt @@ -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" diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 84ebee04669..93958b203d3 100644 --- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -56,4 +56,10 @@ + + + + + +