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 @@ -20,7 +20,9 @@ import android.os.Build
import android.view.View
import android.widget.ImageView
import androidx.annotation.ColorInt
import com.facebook.react.R
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.uimanager.PixelUtil.dpToPx
Expand All @@ -45,6 +47,10 @@ 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.ClipPath
import com.facebook.react.uimanager.style.ClipPathUtils
import com.facebook.react.uimanager.style.GeometryBoxUtil
import com.facebook.react.uimanager.style.GeometryBoxUtil.getGeometryBoxBounds
import com.facebook.react.uimanager.style.LogicalEdge
import com.facebook.react.uimanager.style.OutlineStyle

Expand Down Expand Up @@ -463,6 +469,77 @@ public object BackgroundStyleApplicator {
BackgroundStyleApplicator.setBoxShadow(view, shadowStyles)
}

@JvmStatic
public fun setClipPath(view: View, clipPathMap: ReadableMap?) {
if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) {
return
}

val clipPath = ClipPath.parse(clipPathMap)
view.setTag(R.id.clip_path, clipPath)
view.invalidate()
}

@JvmStatic
public fun getClipPath(view: View): ClipPath? = view.getTag(R.id.clip_path) as? ClipPath

@JvmStatic
public fun applyClipPathIfPresent(view: View, canvas: Canvas) {
val clipPath = getClipPath(view) ?: return
val bounds = getGeometryBoxBounds(view, clipPath.geometryBox, getComputedBorderInsets(view))
val drawingRect = Rect()
view.getDrawingRect(drawingRect)
bounds.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat())

val path: Path? = if (clipPath.shape != null) {
ClipPathUtils.createPathFromBasicShape(clipPath.shape, bounds)
} else if (clipPath.geometryBox != null) {
val composite = getCompositeBackgroundDrawable(view)
val borderRadius = composite?.borderRadius
val computedBorderInsets =
composite?.borderInsets?.resolve(composite.layoutDirection, view.context)

if (borderRadius != null) {
val adjustedBorderRadius = GeometryBoxUtil.adjustBorderRadiusForGeometryBox(
clipPath.geometryBox,
borderRadius.resolve(
composite.layoutDirection,
view.context,
PixelUtil.toDIPFromPixel(drawingRect.width().toFloat()),
PixelUtil.toDIPFromPixel(drawingRect.height().toFloat())
),
computedBorderInsets,
view
)

if (adjustedBorderRadius != null) {
ClipPathUtils.createRoundedRectPath(bounds, adjustedBorderRadius)
} else {
null
}
} else {
null
}
} else {
null
}

if (path != null) {
canvas.clipPath(path)
} else {
canvas.clipRect(bounds)
}
}

@JvmStatic
public fun getComputedBorderInsets(view: View): RectF? {
val composite = getCompositeBackgroundDrawable(view)
if (composite == null) {
return null
}
return composite.borderInsets?.resolve(composite.layoutDirection, view.context)
}

/**
* Sets a feedback underlay drawable for the view.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {
view.setTag(R.id.use_hardware_layer, null);
view.setTag(R.id.filter, null);
view.setTag(R.id.mix_blend_mode, null);
view.setTag(R.id.clip_path, null);
LayerEffectsHelper.apply(view, null, null);

// setShadowColor
Expand Down Expand Up @@ -860,6 +861,11 @@ public void setBoxShadow(T view, @Nullable ReadableArray shadows) {
BackgroundStyleApplicator.setBoxShadow(view, shadows);
}

@ReactProp(name = ViewProps.CLIP_PATH, customType = "ClipPath")
public void setClipPath(T view, @Nullable ReadableMap clipPath) {
BackgroundStyleApplicator.setClipPath(view, clipPath);
}

private void logUnsupportedPropertyWarning(String propName) {
FLog.w(ReactConstants.TAG, "%s doesn't support property '%s'", getName(), propName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public abstract class BaseViewManagerDelegate<

ViewProps.BOX_SHADOW -> mViewManager.setBoxShadow(view, value as ReadableArray?)

ViewProps.CLIP_PATH -> mViewManager.setClipPath(view, value as ReadableMap?)

ViewProps.ELEVATION -> mViewManager.setElevation(view, (value as Double?)?.toFloat() ?: 0.0f)

ViewProps.FILTER -> mViewManager.setFilter(view, value as ReadableArray?)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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 CLIP_PATH: String = "clipPath"
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,255 @@
/*
* 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.style

import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.uimanager.LengthPercentage

private fun getOptionalLengthPercentage(map: ReadableMap, key: String): LengthPercentage? {
return if (map.hasKey(key)) {
LengthPercentage.setFromDynamic(map.getDynamic(key))
} else {
null
}
}

internal data class CircleShape(
val r: LengthPercentage? = null,
val cx: LengthPercentage? = null,
val cy: LengthPercentage? = null,
) {
public companion object {
public fun parse(map: ReadableMap): CircleShape? {
val r = getOptionalLengthPercentage(map, "r")
val cx = getOptionalLengthPercentage(map, "cx")
val cy = getOptionalLengthPercentage(map, "cy")
return CircleShape(r, cx, cy)
}
}
}

internal data class EllipseShape(
val rx: LengthPercentage? = null,
val ry: LengthPercentage? = null,
val cx: LengthPercentage? = null,
val cy: LengthPercentage? = null,
) {
public companion object {
public fun parse(map: ReadableMap): EllipseShape? {
val rx = getOptionalLengthPercentage(map, "rx")
val ry = getOptionalLengthPercentage(map, "ry")
val cx = getOptionalLengthPercentage(map, "cx")
val cy = getOptionalLengthPercentage(map, "cy")
return EllipseShape(rx, ry, cx, cy)
}
}
}

internal data class InsetShape(
val top: LengthPercentage,
val right: LengthPercentage,
val bottom: LengthPercentage,
val left: LengthPercentage,
val borderRadius: LengthPercentage? = null,
) {
public companion object {
public fun parse(map: ReadableMap): InsetShape? {
val top = getOptionalLengthPercentage(map, "top") ?: return null
val right = getOptionalLengthPercentage(map, "right") ?: return null
val bottom = getOptionalLengthPercentage(map, "bottom") ?: return null
val left = getOptionalLengthPercentage(map, "left") ?: return null
val borderRadius = getOptionalLengthPercentage(map, "borderRadius")
return InsetShape(top, right, bottom, left, borderRadius)
}
}
}

public enum class FillRule {
NonZero,
EvenOdd;

public companion object {
public fun fromString(value: String): FillRule {
return when (value.lowercase()) {
"nonzero" -> NonZero
"evenodd" -> EvenOdd
else -> NonZero
}
}
}
}

internal data class PolygonShape(
val points: List<Pair<LengthPercentage, LengthPercentage>>,
val fillRule: FillRule? = null,
) {
public companion object {
public fun parse(map: ReadableMap): PolygonShape? {
if (!map.hasKey("points")) return null
val pointsArray = map.getArray("points") ?: return null
val points = mutableListOf<Pair<LengthPercentage, LengthPercentage>>()

for (i in 0 until pointsArray.size()) {
val pointMap = pointsArray.getMap(i) ?: continue
val x = getOptionalLengthPercentage(pointMap, "x") ?: continue
val y = getOptionalLengthPercentage(pointMap, "y") ?: continue
points.add(Pair(x, y))
}

val fillRule =
if (map.hasKey("fillRule")) {
FillRule.fromString(map.getString("fillRule") ?: "nonzero")
} else {
null
}

return PolygonShape(points, fillRule)
}
}
}

internal data class RectShape(
val top: LengthPercentage,
val right: LengthPercentage,
val bottom: LengthPercentage,
val left: LengthPercentage,
val borderRadius: LengthPercentage? = null,
) {
public companion object {
public fun parse(map: ReadableMap): RectShape? {
val top = getOptionalLengthPercentage(map, "top") ?: return null
val right = getOptionalLengthPercentage(map, "right") ?: return null
val bottom = getOptionalLengthPercentage(map, "bottom") ?: return null
val left = getOptionalLengthPercentage(map, "left") ?: return null
val borderRadius = getOptionalLengthPercentage(map, "borderRadius")
return RectShape(top, right, bottom, left, borderRadius)
}
}
}

internal data class XywhShape(
val x: LengthPercentage,
val y: LengthPercentage,
val width: LengthPercentage,
val height: LengthPercentage,
val borderRadius: LengthPercentage? = null,
) {
public companion object {
public fun parse(map: ReadableMap): XywhShape? {
val x = getOptionalLengthPercentage(map, "x") ?: return null
val y = getOptionalLengthPercentage(map, "y") ?: return null
val width = getOptionalLengthPercentage(map, "width") ?: return null
val height = getOptionalLengthPercentage(map, "height") ?: return null
val borderRadius = getOptionalLengthPercentage(map, "borderRadius")
return XywhShape(x, y, width, height, borderRadius)
}
}
}

public sealed class BasicShape {
internal data class Circle(val shape: CircleShape) : BasicShape()

internal data class Ellipse(val shape: EllipseShape) : BasicShape()

internal data class Inset(val shape: InsetShape) : BasicShape()

internal data class Polygon(val shape: PolygonShape) : BasicShape()

internal data class Rect(val shape: RectShape) : BasicShape()

internal data class Xywh(val shape: XywhShape) : BasicShape()

public companion object {
public fun parse(map: ReadableMap): BasicShape? {
if (!map.hasKey("type")) return null
val type = map.getString("type") ?: return null

return when (type.lowercase()) {
"circle" -> {
val circle = CircleShape.parse(map) ?: return null
Circle(circle)
}
"ellipse" -> {
val ellipse = EllipseShape.parse(map) ?: return null
Ellipse(ellipse)
}
"inset" -> {
val inset = InsetShape.parse(map) ?: return null
Inset(inset)
}
"polygon" -> {
val polygon = PolygonShape.parse(map) ?: return null
Polygon(polygon)
}
"rect" -> {
val rect = RectShape.parse(map) ?: return null
Rect(rect)
}
"xywh" -> {
val xywh = XywhShape.parse(map) ?: return null
Xywh(xywh)
}
else -> null
}
}
}
}

public enum class GeometryBox {
MarginBox,
BorderBox,
ContentBox,
PaddingBox,
FillBox,
StrokeBox,
ViewBox;

public companion object {
public fun fromString(value: String): GeometryBox {
return when (value.lowercase()) {
"margin-box" -> MarginBox
"border-box" -> BorderBox
"content-box" -> ContentBox
"padding-box" -> PaddingBox
"fill-box" -> FillBox
"stroke-box" -> StrokeBox
"view-box" -> ViewBox
else -> BorderBox
}
}
}
}

public data class ClipPath(
val shape: BasicShape? = null,
val geometryBox: GeometryBox? = null,
) {
public companion object {
public fun parse(map: ReadableMap?): ClipPath? {
if (map == null) return null

val shape =
if (map.hasKey("shape") && map.getType("shape") == ReadableType.Map) {
val shapeMap = map.getMap("shape")
if (shapeMap != null) BasicShape.parse(shapeMap) else null
} else {
null
}

val geometryBox =
if (map.hasKey("geometryBox")) {
GeometryBox.fromString(map.getString("geometryBox") ?: "border-box")
} else {
null
}

return ClipPath(shape, geometryBox)
}
}
}
Loading