/* * 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.uimanager import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Drawable import android.os.Build import android.view.View import android.widget.ImageView import androidx.annotation.ColorInt import com.facebook.react.bridge.ReadableArray import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.PixelUtil.pxToDp import com.facebook.react.uimanager.common.UIManagerType import com.facebook.react.uimanager.common.ViewUtil import com.facebook.react.uimanager.drawable.BackgroundDrawable import com.facebook.react.uimanager.drawable.BackgroundImageDrawable import com.facebook.react.uimanager.drawable.BorderDrawable import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable import com.facebook.react.uimanager.drawable.MIN_INSET_BOX_SHADOW_SDK_VERSION import com.facebook.react.uimanager.drawable.MIN_OUTSET_BOX_SHADOW_SDK_VERSION import com.facebook.react.uimanager.drawable.OutlineDrawable import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable import com.facebook.react.uimanager.style.BackgroundImageLayer import com.facebook.react.uimanager.style.BackgroundPosition import com.facebook.react.uimanager.style.BackgroundRepeat import com.facebook.react.uimanager.style.BackgroundSize import com.facebook.react.uimanager.style.BorderInsets import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.uimanager.style.BorderRadiusStyle import com.facebook.react.uimanager.style.BorderStyle import com.facebook.react.uimanager.style.BoxShadow import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.uimanager.style.OutlineStyle /** * Utility object responsible for applying backgrounds, borders, and related visual effects to * Android views. * * This object provides methods to manage background colors, images, borders, outlines, and box * shadows for React Native views. It handles the complex layering and composition of these visual * properties by managing [CompositeBackgroundDrawable] instances. */ @OptIn(UnstableReactNativeAPI::class) public object BackgroundStyleApplicator { /** * Sets the background color of the view. * * @param view The view to apply the background color to * @param color The color to set, or null to remove the background color */ @JvmStatic public fun setBackgroundColor(view: View, @ColorInt color: Int?): Unit { // No color to set, and no color already set if ( (color == null || color == Color.TRANSPARENT) && view.background !is CompositeBackgroundDrawable ) { return } ensureBackgroundDrawable(view).backgroundColor = color ?: Color.TRANSPARENT } /** * Sets the background image layers for the view. * * @param view The view to apply the background images to * @param backgroundImageLayers The list of background image layers to apply, or null to remove */ @JvmStatic public fun setBackgroundImage( view: View, backgroundImageLayers: List?, ): Unit { ensureBackgroundImageDrawable(view).backgroundImageLayers = backgroundImageLayers } @JvmStatic internal fun setBackgroundSize(view: View, backgroundSizes: List?): Unit { ensureBackgroundImageDrawable(view).backgroundSize = backgroundSizes } @JvmStatic internal fun setBackgroundPosition( view: View, backgroundPositions: List?, ): Unit { ensureBackgroundImageDrawable(view).backgroundPosition = backgroundPositions } @JvmStatic internal fun setBackgroundRepeat(view: View, backgroundRepeats: List?): Unit { ensureBackgroundImageDrawable(view).backgroundRepeat = backgroundRepeats } /** * Gets the background color of the view. * * @param view The view to get the background color from * @return The background color, or null if no background color is set */ @JvmStatic @ColorInt public fun getBackgroundColor(view: View): Int? { return getBackground(view)?.backgroundColor } /** * Sets the border width for a specific edge of the view. * * @param view The view to apply the border width to * @param edge The logical edge (start, end, top, bottom, etc.) to set the width for * @param width The border width in DIPs, or null to remove */ @JvmStatic public fun setBorderWidth(view: View, edge: LogicalEdge, width: Float?): Unit { val composite = ensureCompositeBackgroundDrawable(view) composite.borderInsets = composite.borderInsets ?: BorderInsets() composite.borderInsets?.setBorderWidth(edge, width) ensureBorderDrawable(view).setBorderWidth(edge.toSpacingType(), width?.dpToPx() ?: Float.NaN) composite.background?.borderInsets = composite.borderInsets composite.backgroundImage?.borderInsets = composite.borderInsets composite.border?.borderInsets = composite.borderInsets composite.background?.invalidateSelf() composite.backgroundImage?.invalidateSelf() composite.border?.invalidateSelf() composite.borderInsets = composite.borderInsets ?: BorderInsets() composite.borderInsets?.setBorderWidth(edge, width) if (Build.VERSION.SDK_INT >= MIN_INSET_BOX_SHADOW_SDK_VERSION) { for (shadow in composite.innerShadows.filterIsInstance()) { shadow.borderInsets = composite.borderInsets } } } /** * Gets the border width for a specific edge of the view. * * @param view The view to get the border width from * @param edge The logical edge to get the width for * @return The border width in DIPs, or null if not set */ @JvmStatic public fun getBorderWidth(view: View, edge: LogicalEdge): Float? { val width = getBorder(view)?.borderWidth?.getRaw(edge.toSpacingType()) if (width == null || width.isNaN()) { return null } else { return width.pxToDp() } } /** * Sets the border color for a specific edge of the view. * * @param view The view to apply the border color to * @param edge The logical edge to set the color for * @param color The border color, or null to remove */ @JvmStatic public fun setBorderColor(view: View, edge: LogicalEdge, @ColorInt color: Int?): Unit { ensureBorderDrawable(view).setBorderColor(edge, color) } /** * Gets the border color for a specific edge of the view. * * @param view The view to get the border color from * @param edge The logical edge to get the color for * @return The border color, or null if not set */ @JvmStatic @ColorInt public fun getBorderColor(view: View, edge: LogicalEdge): Int? { return getBorder(view)?.getBorderColor(edge) } /** * Sets the border radius for a specific corner of the view. * * @param view The view to apply the border radius to * @param corner The corner property to set the radius for * @param radius The border radius value (length or percentage), or null to remove */ @JvmStatic public fun setBorderRadius( view: View, corner: BorderRadiusProp, radius: LengthPercentage?, ): Unit { val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) compositeBackgroundDrawable.borderRadius = compositeBackgroundDrawable.borderRadius ?: BorderRadiusStyle() compositeBackgroundDrawable.borderRadius?.set(corner, radius) if (view is ImageView) { ensureBackgroundDrawable(view) } compositeBackgroundDrawable.background?.borderRadius = compositeBackgroundDrawable.borderRadius compositeBackgroundDrawable.backgroundImage?.borderRadius = compositeBackgroundDrawable.borderRadius compositeBackgroundDrawable.border?.borderRadius = compositeBackgroundDrawable.borderRadius compositeBackgroundDrawable.background?.invalidateSelf() compositeBackgroundDrawable.backgroundImage?.invalidateSelf() compositeBackgroundDrawable.border?.invalidateSelf() if (Build.VERSION.SDK_INT >= MIN_OUTSET_BOX_SHADOW_SDK_VERSION) { for (shadow in compositeBackgroundDrawable.outerShadows.filterIsInstance()) { shadow.borderRadius = compositeBackgroundDrawable.borderRadius } } if (Build.VERSION.SDK_INT >= MIN_INSET_BOX_SHADOW_SDK_VERSION) { for (shadow in compositeBackgroundDrawable.innerShadows.filterIsInstance()) { shadow.borderRadius = compositeBackgroundDrawable.borderRadius } } compositeBackgroundDrawable.outline?.borderRadius = compositeBackgroundDrawable.borderRadius compositeBackgroundDrawable.invalidateSelf() } /** * Gets the border radius for a specific corner of the view. * * @param view The view to get the border radius from * @param corner The corner property to get the radius for * @return The border radius value, or null if not set */ @JvmStatic public fun getBorderRadius(view: View, corner: BorderRadiusProp): LengthPercentage? { return getCompositeBackgroundDrawable(view)?.borderRadius?.get(corner) } /** * Sets the border style for the view. * * @param view The view to apply the border style to * @param borderStyle The border style (solid, dashed, dotted), or null to remove */ @JvmStatic public fun setBorderStyle(view: View, borderStyle: BorderStyle?) { ensureBorderDrawable(view).borderStyle = borderStyle } /** * Gets the border style of the view. * * @param view The view to get the border style from * @return The border style, or null if not set */ @JvmStatic public fun getBorderStyle(view: View): BorderStyle? { return getBorder(view)?.borderStyle } /** * Sets the outline color for the view (Fabric only). * * @param view The view to apply the outline color to * @param outlineColor The outline color, or null to remove */ @JvmStatic public fun setOutlineColor(view: View, @ColorInt outlineColor: Int?) { if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { return } val outline = ensureOutlineDrawable(view) if (outlineColor != null) { outline.outlineColor = outlineColor } } /** * Gets the outline color of the view. * * @param view The view to get the outline color from * @return The outline color, or null if not set */ @JvmStatic public fun getOutlineColor(view: View): Int? = getOutlineDrawable(view)?.outlineColor /** * Sets the outline offset for the view (Fabric only). * * @param view The view to apply the outline offset to * @param outlineOffset The outline offset in DIPs */ @JvmStatic public fun setOutlineOffset(view: View, outlineOffset: Float): Unit { if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { return } val outline = ensureOutlineDrawable(view) outline.outlineOffset = outlineOffset.dpToPx() } /** * Gets the outline offset of the view. * * @param view The view to get the outline offset from * @return The outline offset in pixels, or null if not set */ public fun getOutlineOffset(view: View): Float? = getOutlineDrawable(view)?.outlineOffset /** * Sets the outline style for the view (Fabric only). * * @param view The view to apply the outline style to * @param outlineStyle The outline style (solid, dashed, dotted), or null to remove */ @JvmStatic public fun setOutlineStyle(view: View, outlineStyle: OutlineStyle?): Unit { if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { return } val outline = ensureOutlineDrawable(view) if (outlineStyle != null) { outline.outlineStyle = outlineStyle } } /** * Gets the outline style of the view. * * @param view The view to get the outline style from * @return The outline style, or null if not set */ public fun getOutlineStyle(view: View): OutlineStyle? = getOutlineDrawable(view)?.outlineStyle /** * Sets the outline width for the view (Fabric only). * * @param view The view to apply the outline width to * @param width The outline width in DIPs */ @JvmStatic public fun setOutlineWidth(view: View, width: Float) { if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { return } val outline = ensureOutlineDrawable(view) outline.outlineWidth = width.dpToPx() } /** * Gets the outline width of the view. * * @param view The view to get the outline width from * @return The outline width in pixels, or null if not set */ public fun getOutlineWidth(view: View): Float? = getOutlineDrawable(view)?.outlineOffset /** * Sets box shadows for the view (Fabric only). * * @param view The view to apply box shadows to * @param shadows The list of box shadow styles to apply */ @JvmStatic public fun setBoxShadow(view: View, shadows: List) { if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { return } var innerShadows = mutableListOf() var outerShadows = mutableListOf() val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) val borderInsets = compositeBackgroundDrawable.borderInsets val borderRadius = compositeBackgroundDrawable.borderRadius /** * z-ordering of user-provided shadow-list is opposite direction of LayerDrawable z-ordering * https://drafts.csswg.org/css-backgrounds/#shadow-layers */ for (boxShadow in shadows) { val offsetX = boxShadow.offsetX val offsetY = boxShadow.offsetY val color = boxShadow.color ?: Color.BLACK val blurRadius = boxShadow.blurRadius ?: 0f val spreadDistance = boxShadow.spreadDistance ?: 0f val inset = boxShadow.inset ?: false if (inset && Build.VERSION.SDK_INT >= MIN_INSET_BOX_SHADOW_SDK_VERSION) { innerShadows.add( InsetBoxShadowDrawable( context = view.context, borderRadius = borderRadius, borderInsets = borderInsets, shadowColor = color, offsetX = offsetX, offsetY = offsetY, blurRadius = blurRadius, spread = spreadDistance, ) ) } else if (!inset && Build.VERSION.SDK_INT >= MIN_OUTSET_BOX_SHADOW_SDK_VERSION) { outerShadows.add( OutsetBoxShadowDrawable( context = view.context, borderRadius = borderRadius, shadowColor = color, offsetX = offsetX, offsetY = offsetY, blurRadius = blurRadius, spread = spreadDistance, ) ) } } view.background = ensureCompositeBackgroundDrawable(view) .withNewShadows(outerShadows = outerShadows, innerShadows = innerShadows) } /** * Sets box shadows for the view from a ReadableArray (Fabric only). * * @param view The view to apply box shadows to * @param shadows The array of box shadow definitions, or null to remove all shadows */ @JvmStatic public fun setBoxShadow(view: View, shadows: ReadableArray?) { if (shadows == null) { BackgroundStyleApplicator.setBoxShadow(view, emptyList()) return } val shadowStyles = mutableListOf() for (i in 0.. Unit)?, ) { val drawingRect = Rect() view.getDrawingRect(drawingRect) val composite = getCompositeBackgroundDrawable(view) if (composite == null) { canvas.clipRect(drawingRect) drawContent?.invoke() return } val paddingBoxRect = RectF() val computedBorderInsets = composite.borderInsets?.resolve(composite.layoutDirection, view.context) paddingBoxRect.left = composite.bounds.left + (computedBorderInsets?.left?.dpToPx() ?: 0f) paddingBoxRect.top = composite.bounds.top + (computedBorderInsets?.top?.dpToPx() ?: 0f) paddingBoxRect.right = composite.bounds.right - (computedBorderInsets?.right?.dpToPx() ?: 0f) paddingBoxRect.bottom = composite.bounds.bottom - (computedBorderInsets?.bottom?.dpToPx() ?: 0f) if (composite.borderRadius?.hasRoundedBorders() == true) { val paddingBoxPath = createPaddingBoxPath( view, composite, paddingBoxRect, computedBorderInsets, ) paddingBoxPath.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) // On Android 28 and below, use antialiased clipping with Porter-Duff compositing. On newer // Android versions, use the standard clipPath. if ( ReactNativeFeatureFlags.enableAndroidAntialiasedBorderRadiusClipping() && Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && view.width > 0 && view.height > 0 && drawContent != null ) { clipWithAntiAliasing( view, canvas, paddingBoxPath, drawContent, ) } else { canvas.clipPath(paddingBoxPath) drawContent?.invoke() } } else { paddingBoxRect.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) canvas.clipRect(paddingBoxRect) drawContent?.invoke() } } /** * Applies antialiased clipping using Porter-Duff compositing for Android 28 and below. This draws * content to a layer, then applies an antialiased mask to clip it. */ private fun clipWithAntiAliasing( view: View, canvas: Canvas, paddingBoxPath: Path, drawContent: () -> Unit, ) { // Save the layer for Porter-Duff compositing val saveCount = canvas.saveLayer(0f, 0f, view.width.toFloat(), view.height.toFloat(), null) // Draw the content first drawContent() val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG) maskPaint.style = Paint.Style.FILL // Transparent pixels with INVERSE_WINDING only works on API 28 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) maskPaint.color = Color.TRANSPARENT paddingBoxPath.setFillType(Path.FillType.INVERSE_WINDING) canvas.drawPath(paddingBoxPath, maskPaint) } else { // Create an inverse path: outer rect minus the rounded rect (using even-odd fill rule) val inversePath = Path() inversePath.addRect(0f, 0f, view.width.toFloat(), view.height.toFloat(), Path.Direction.CW) inversePath.addPath(paddingBoxPath) inversePath.setFillType(Path.FillType.EVEN_ODD) // Use DST_OUT to remove content where the mask is drawn (outside the rounded rect) maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) maskPaint.color = Color.BLACK canvas.drawPath(inversePath, maskPaint) } // Restore the layer canvas.restoreToCount(saveCount) } /** * Resets the background styling of the view to its original state. * * This removes any CompositeBackgroundDrawable and restores the original background. * * @param view The view to reset */ @JvmStatic public fun reset(view: View) { if (view.background is CompositeBackgroundDrawable) { view.background = (view.background as CompositeBackgroundDrawable).originalBackground } } private fun ensureCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable { if (view.background is CompositeBackgroundDrawable) { return view.background as CompositeBackgroundDrawable } val compositeDrawable = CompositeBackgroundDrawable(context = view.context, originalBackground = view.background) view.background = compositeDrawable return compositeDrawable } private fun getCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable? = view.background as? CompositeBackgroundDrawable private fun ensureBackgroundDrawable(view: View): BackgroundDrawable { val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) var background = compositeBackgroundDrawable.background return if (background != null) { background } else { background = BackgroundDrawable( view.context, compositeBackgroundDrawable.borderRadius, compositeBackgroundDrawable.borderInsets, ) view.background = compositeBackgroundDrawable.withNewBackground(background) background } } private fun getBackground(view: View): BackgroundDrawable? = getCompositeBackgroundDrawable(view)?.background private fun ensureBackgroundImageDrawable(view: View): BackgroundImageDrawable { val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) var backgroundImage = compositeBackgroundDrawable.backgroundImage return if (backgroundImage != null) { backgroundImage } else { backgroundImage = BackgroundImageDrawable( view.context, compositeBackgroundDrawable.borderRadius, compositeBackgroundDrawable.borderInsets, ) view.background = compositeBackgroundDrawable.withNewBackgroundImage(backgroundImage) backgroundImage } } private fun getBackgroundImage(view: View): BackgroundImageDrawable? = getCompositeBackgroundDrawable(view)?.backgroundImage private fun getBorder(view: View): BorderDrawable? = getCompositeBackgroundDrawable(view)?.border private fun ensureBorderDrawable(view: View): BorderDrawable { val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) var border = compositeBackgroundDrawable.border if (border == null) { border = BorderDrawable( context = view.context, borderRadius = compositeBackgroundDrawable.borderRadius, borderWidth = Spacing(0f), borderStyle = BorderStyle.SOLID, borderInsets = compositeBackgroundDrawable.borderInsets, ) view.background = compositeBackgroundDrawable.withNewBorder(border) } return border } private fun ensureOutlineDrawable(view: View): OutlineDrawable { val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) var outline = compositeBackgroundDrawable.outline if (outline == null) { val borderRadius = compositeBackgroundDrawable.borderRadius outline = OutlineDrawable( context = view.context, borderRadius = borderRadius, outlineColor = Color.BLACK, outlineOffset = 0f, outlineStyle = OutlineStyle.SOLID, outlineWidth = 0f, ) view.background = compositeBackgroundDrawable.withNewOutline(outline) } return outline } private fun getOutlineDrawable(view: View): OutlineDrawable? = getCompositeBackgroundDrawable(view)?.outline /** * Here, "inner" refers to the border radius on the inside of the border. So it ends up being the * "outer" border radius inset by the respective width. */ private fun getInnerBorderRadius(computedRadius: Float?, borderWidth: Float?): Float { return ((computedRadius ?: 0f) - (borderWidth ?: 0f)).coerceAtLeast(0f) } private fun createPaddingBoxPath( view: View, composite: CompositeBackgroundDrawable, paddingBoxRect: RectF, computedBorderInsets: RectF?, ): Path { val computedBorderRadius = composite.borderRadius?.resolve( composite.layoutDirection, view.context, PixelUtil.toDIPFromPixel(composite.bounds.width().toFloat()), PixelUtil.toDIPFromPixel(composite.bounds.height().toFloat()), ) val paddingBoxPath = Path() val innerTopLeftRadiusX = getInnerBorderRadius( computedBorderRadius?.topLeft?.horizontal?.dpToPx(), computedBorderInsets?.left?.dpToPx(), ) val innerTopLeftRadiusY = getInnerBorderRadius( computedBorderRadius?.topLeft?.vertical?.dpToPx(), computedBorderInsets?.top?.dpToPx(), ) val innerTopRightRadiusX = getInnerBorderRadius( computedBorderRadius?.topRight?.horizontal?.dpToPx(), computedBorderInsets?.right?.dpToPx(), ) val innerTopRightRadiusY = getInnerBorderRadius( computedBorderRadius?.topRight?.vertical?.dpToPx(), computedBorderInsets?.top?.dpToPx(), ) val innerBottomRightRadiusX = getInnerBorderRadius( computedBorderRadius?.bottomRight?.horizontal?.dpToPx(), computedBorderInsets?.right?.dpToPx(), ) val innerBottomRightRadiusY = getInnerBorderRadius( computedBorderRadius?.bottomRight?.vertical?.dpToPx(), computedBorderInsets?.bottom?.dpToPx(), ) val innerBottomLeftRadiusX = getInnerBorderRadius( computedBorderRadius?.bottomLeft?.horizontal?.dpToPx(), computedBorderInsets?.left?.dpToPx(), ) val innerBottomLeftRadiusY = getInnerBorderRadius( computedBorderRadius?.bottomLeft?.vertical?.dpToPx(), computedBorderInsets?.bottom?.dpToPx(), ) paddingBoxPath.addRoundRect( paddingBoxRect, floatArrayOf( innerTopLeftRadiusX, innerTopLeftRadiusY, innerTopRightRadiusX, innerTopRightRadiusY, innerBottomRightRadiusX, innerBottomRightRadiusY, innerBottomLeftRadiusX, innerBottomLeftRadiusY, ), Path.Direction.CW, ) return paddingBoxPath } }