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 @@ + + + + + +