Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {

view.setTag(R.id.use_hardware_layer, null);
view.setTag(R.id.filter, null);
view.setTag(R.id.backdrop_filter, null);
view.setTag(R.id.mix_blend_mode, null);
LayerEffectsHelper.apply(view, null, null);

Expand Down Expand Up @@ -242,6 +243,14 @@ public void setFilter(@NonNull T view, @Nullable ReadableArray filter) {
view.setTag(R.id.filter, filter);
}

@ReactProp(name = ViewProps.BACKDROP_FILTER, customType = "Filter")
public void setBackdropFilter(@NonNull T view, @Nullable ReadableArray backdropFilter) {
view.setTag(R.id.backdrop_filter, backdropFilter);
if (view.getParent() instanceof View) {
((View) view.getParent()).invalidate();
}
}

@ReactProp(name = ViewProps.MIX_BLEND_MODE)
public void setMixBlendMode(@NonNull T view, @Nullable String mixBlendMode) {
view.setTag(R.id.mix_blend_mode, BlendModeHelper.parseMixBlendMode(mixBlendMode));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ public abstract class BaseViewManagerDelegate<

ViewProps.FILTER -> mViewManager.setFilter(view, value as ReadableArray?)

ViewProps.BACKDROP_FILTER -> mViewManager.setBackdropFilter(view, value as ReadableArray?)

ViewProps.MIX_BLEND_MODE -> mViewManager.setMixBlendMode(view, value as String?)

ViewProps.SHADOW_COLOR ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,18 @@ internal object FilterHelper {

// https://www.w3.org/TR/filter-effects-1/#blurEquivalent
fun createBlurEffect(sigma: Float, chainedEffects: RenderEffect? = null): RenderEffect? {
if (sigma <= 0.5) {
// RenderEffect.createBlurEffect takes sigma (standard deviation) directly — it passes
// through to Skia's SkImageFilters::Blur(sigmaX, sigmaY). Convert dp→px here; do NOT
// apply sigmaToRadius (that formula is for HWUI's box-filter BlurMaskFilter, not RenderEffect).
val pxSigma = PixelUtil.toPixelFromDIP(sigma)
if (pxSigma <= 0.5f) {
return null
}

val radius: Float = sigmaToRadius(sigma)
return if (chainedEffects == null) {
RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.DECAL)
RenderEffect.createBlurEffect(pxSigma, pxSigma, Shader.TileMode.DECAL)
} else {
RenderEffect.createBlurEffect(radius, radius, chainedEffects, Shader.TileMode.DECAL)
RenderEffect.createBlurEffect(pxSigma, pxSigma, chainedEffects, Shader.TileMode.DECAL)
}
}

Expand Down Expand Up @@ -199,7 +202,7 @@ internal object FilterHelper {
val color: Int = if (filterValues.hasKey("color")) filterValues.getInt("color") else Color.BLACK
val radius: Float =
if (filterValues.hasKey("standardDeviation"))
sigmaToRadius(filterValues.getDouble("standardDeviation").toFloat())
PixelUtil.toPixelFromDIP(filterValues.getDouble("standardDeviation").toFloat())
else 0f

return createDropShadowEffect(offsetX, offsetY, radius, color, chainedEffects)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public object ViewProps {
public const val BORDER_START_COLOR: String = "borderStartColor"
public const val BORDER_END_COLOR: String = "borderEndColor"
public const val BOX_SHADOW: String = "boxShadow"
public const val BACKDROP_FILTER: String = "backdropFilter"
public const val FILTER: String = "filter"
public const val MIX_BLEND_MODE: String = "mixBlendMode"
public const val OUTLINE_COLOR: String = "outlineColor"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) Meta Platforms, Inc. and 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.annotation.TargetApi
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RenderEffect
import android.graphics.RenderNode
import android.os.Build
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.uimanager.FilterHelper

/**
* Helper object for applying CSS backdrop-filter effects on Android.
*
* Applies filter effects to the composited backdrop (the pixels behind a view) captured as a
* [Bitmap] snapshot. Color-matrix filters (brightness, contrast, grayscale, hue-rotate, invert,
* opacity, saturate, sepia) work on all supported API levels. Blur and drop-shadow require API 31+
* via [RenderEffect] drawn into the hardware-accelerated canvas via [RenderNode].
*/
@SuppressLint("UseRequiresApi")
internal object BackdropFilterHelper {

/**
* Draws a filtered region of [source] directly onto [canvas] at the child's position.
*
* @param canvas Hardware-accelerated canvas of the parent [ReactViewGroup].
* @param source Full-size backdrop snapshot bitmap (same dimensions as the parent view).
* @param destLeft Left coordinate of the child in parent space (canvas destination).
* @param destTop Top coordinate of the child in parent space (canvas destination).
* @param srcLeft Clamped left bound of the crop region within [source].
* @param srcTop Clamped top bound of the crop region within [source].
* @param srcRight Clamped right bound of the crop region within [source].
* @param srcBottom Clamped bottom bound of the crop region within [source].
* @param filters ReadableArray of filter descriptors as produced by processFilter.js.
*/
fun drawFilteredBackdrop(
canvas: Canvas,
source: Bitmap,
destLeft: Int,
destTop: Int,
srcLeft: Int,
srcTop: Int,
srcRight: Int,
srcBottom: Int,
filters: ReadableArray,
) {
val cropWidth = srcRight - srcLeft
val cropHeight = srcBottom - srcTop
if (cropWidth <= 0 || cropHeight <= 0) return

val cropped = Bitmap.createBitmap(source, srcLeft, srcTop, cropWidth, cropHeight)
try {
when {
FilterHelper.isOnlyColorMatrixFilters(filters) -> {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.colorFilter = FilterHelper.parseColorMatrixFilters(filters)
canvas.drawBitmap(cropped, destLeft.toFloat(), destTop.toFloat(), paint)
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
drawWithRenderEffect(canvas, cropped, destLeft.toFloat(), destTop.toFloat(), filters)
}
else -> {
// Pre-API 31: blur and drop-shadow are not supported; draw the backdrop unfiltered so
// at least the element's transparency is composited correctly.
canvas.drawBitmap(cropped, destLeft.toFloat(), destTop.toFloat(), null)
}
}
} finally {
cropped.recycle()
}
}

@TargetApi(Build.VERSION_CODES.S)
private fun drawWithRenderEffect(
canvas: Canvas,
source: Bitmap,
destLeft: Float,
destTop: Float,
filters: ReadableArray,
) {
val renderEffect: RenderEffect =
FilterHelper.parseFilters(filters)
?: run {
canvas.drawBitmap(source, destLeft, destTop, null)
return
}

val renderNode = RenderNode("backdropFilter")
renderNode.setPosition(0, 0, source.width, source.height)
renderNode.setRenderEffect(renderEffect)

val recordCanvas = renderNode.beginRecording()
recordCanvas.drawBitmap(source, 0f, 0f, null)
renderNode.endRecording()

canvas.save()
canvas.translate(destLeft, destTop)
canvas.drawRenderNode(renderNode)
canvas.restore()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BlendMode
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
Expand All @@ -29,6 +31,7 @@ import com.facebook.react.R
import com.facebook.react.bridge.ReactNoCrashSoftException
import com.facebook.react.bridge.ReactSoftExceptionLogger
import com.facebook.react.bridge.ReactSoftExceptionLogger.logSoftException
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil.assertOnUiThread
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
Expand Down Expand Up @@ -988,11 +991,45 @@ public open class ReactViewGroup public constructor(context: Context?) :
}
}

// Snapshot of the parent's composited content (background + previous siblings) used to apply
// backdrop-filter effects on children that request them.
private var backdropBitmap: Bitmap? = null
private var backdropCanvas: Canvas? = null
private var hasBackdropFilterChild = false

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
backdropBitmap?.recycle()
backdropBitmap = null
backdropCanvas = null
}

override fun dispatchDraw(canvas: Canvas) {
if (_overflow != Overflow.VISIBLE || getTag(R.id.filter) != null) {
clipToPaddingBox(this, canvas)
}

hasBackdropFilterChild =
(0 until childCount).any { getChildAt(it).getTag(R.id.backdrop_filter) != null }

if (hasBackdropFilterChild && width > 0 && height > 0) {
val bmp =
backdropBitmap?.takeIf { it.width == width && it.height == height }
?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also {
backdropBitmap?.recycle()
backdropBitmap = it
}
bmp.eraseColor(Color.WHITE)
val snapCanvas = Canvas(bmp)
backdropCanvas = snapCanvas
background?.draw(snapCanvas)
}

super.dispatchDraw(canvas)

if (hasBackdropFilterChild) {
backdropCanvas = null
}
}

override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
Expand All @@ -1002,6 +1039,31 @@ public open class ReactViewGroup public constructor(context: Context?) :
enableZ(canvas, true)
}

val backdropFilter: ReadableArray? =
if (hasBackdropFilterChild) child.getTag(R.id.backdrop_filter) as? ReadableArray else null
val snapBitmap = backdropBitmap
if (backdropFilter != null && snapBitmap != null) {
val childLeft = child.left
val childTop = child.top
val srcLeft = childLeft.coerceAtLeast(0).coerceAtMost(snapBitmap.width)
val srcTop = childTop.coerceAtLeast(0).coerceAtMost(snapBitmap.height)
val srcRight = child.right.coerceAtLeast(0).coerceAtMost(snapBitmap.width)
val srcBottom = child.bottom.coerceAtLeast(0).coerceAtMost(snapBitmap.height)
if (srcRight > srcLeft && srcBottom > srcTop) {
BackdropFilterHelper.drawFilteredBackdrop(
canvas,
snapBitmap,
childLeft,
childTop,
srcLeft,
srcTop,
srcRight,
srcBottom,
backdropFilter,
)
}
}

var mixBlendMode: BlendMode? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer(this)) {
mixBlendMode = child.getTag(R.id.mix_blend_mode) as? BlendMode
Expand All @@ -1025,6 +1087,12 @@ public open class ReactViewGroup public constructor(context: Context?) :
canvas.restore()
}

// Update snapshot so subsequent backdrop-filter siblings see this child's pixels.
val snapCanvas = backdropCanvas
if (hasBackdropFilterChild && snapCanvas != null && backdropFilter == null) {
super.drawChild(snapCanvas, child, drawingTime)
}

if (drawWithZ) {
enableZ(canvas, false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
<!-- tag is used to store graphical filter effects to apply to the view -->
<item type="id" name="filter"/>

<!-- tag is used to store backdrop filter effects to apply behind the view -->
<item type="id" name="backdrop_filter"/>

<!-- tag is used to store mixBlendMode effects to apply to the view -->
<item type="id" name="mix_blend_mode"/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ BaseViewProps::BaseViewProps(
{})),
filter(
convertRawProp(context, rawProps, "filter", sourceProps.filter, {})),
backdropFilter(convertRawProp(
context,
rawProps,
"backdropFilter",
sourceProps.backdropFilter,
{})),
backgroundImage(convertRawProp(
context,
rawProps,
Expand Down Expand Up @@ -390,6 +396,7 @@ void BaseViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(outlineStyle);
RAW_SET_PROP_SWITCH_CASE_BASIC(outlineWidth);
RAW_SET_PROP_SWITCH_CASE_BASIC(filter);
RAW_SET_PROP_SWITCH_CASE_BASIC(backdropFilter);
RAW_SET_PROP_SWITCH_CASE_BASIC(boxShadow);
RAW_SET_PROP_SWITCH_CASE_BASIC(mixBlendMode);
// events field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
// Filter
std::vector<FilterFunction> filter{};

// Backdrop Filter
std::vector<FilterFunction> backdropFilter{};

// Background Image
std::vector<BackgroundImage> backgroundImage{};

Expand Down
Loading