Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c95c0d1
feat: add clip-path support for Android
Nov 26, 2025
227bc80
refactor: change visibility to internal
Dec 2, 2025
770cafb
refactor: update drawing methods to always use draw()
Dec 2, 2025
31438bb
refactor: move file to correct dir
Dec 2, 2025
70ed30c
fix: offset bounds by drawingRect topLeft
Dec 2, 2025
fd1e395
refactor: run canvas save/restore conditionally
Dec 2, 2025
d842742
feat(android): storing computed margin and padding information
Dec 19, 2025
c1b43f8
fix(android): cleanup
Dec 19, 2025
4a7a289
refactor(android): remove redundant clip path handling in dispatchDraw
Dec 29, 2025
15a1bfc
refactor(android): remove setClipPath method from ReactViewManager
Dec 29, 2025
7a4ddf0
feat(android): cleaner clip path application in drawing methods
Dec 29, 2025
e89227e
feat(android): move retreiving margin and padding insets to FabricUiM…
Dec 29, 2025
780ad47
feat(android): add NeedsComputedBoxModel trait for clipPath handling
Dec 29, 2025
158ea50
refactor(android): remove of getClipPath method
Dec 29, 2025
b0b0fd0
refactor(android): remove unnecessary blank line
Jan 8, 2026
3ce06a8
refactor(android): remove unused imports in ReactTextView
Jan 12, 2026
fb47896
feat(android): layout insets calculation from YogaStylableProps
Jan 14, 2026
9dda4ec
feat(cpp): add shared C++ clipPath types and prop conversions
marcinszalski-callstack Sep 17, 2026
5d3d585
Merge branch 'main' into feat/clip-path-android
marcinszalski-callstack Oct 15, 2026
b3e68e0
Fix formatting
marcinszalski-callstack Oct 15, 2026
0e5d914
Fix ClipPath Android build: add missing std headers and switch defaults
marcinszalski-callstack Oct 16, 2026
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 @@ -21,6 +21,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.graphics.RectF;
import android.os.SystemClock;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
Expand Down Expand Up @@ -1533,6 +1534,27 @@ public Map<String, Long> getPerformanceCounters() {
return performanceCounters;
}

@Nullable
private RectF computeInsetsToRectF(@Nullable float[] insets, int surfaceId, int viewTag) {
if (insets == null || surfaceId == View.NO_ID || viewTag == View.NO_ID) {
return null;
}
return new RectF(insets[0], insets[1], insets[2], insets[3]);
}

@Nullable
public RectF getComputedMarginInsets(int surfaceId, int viewTag) {
float[] insets = mBinding != null ? mBinding.getComputedMarginInsets(surfaceId, viewTag) : null;
return computeInsetsToRectF(insets, surfaceId, viewTag);
}

@Nullable
public RectF getComputedPaddingInsets(int surfaceId, int viewTag) {
float[] insets =
mBinding != null ? mBinding.getComputedPaddingInsets(surfaceId, viewTag) : null;
return computeInsetsToRectF(insets, surfaceId, viewTag);
}

private class MountItemDispatchListener implements MountItemDispatcher.ItemDispatchListener {
@UiThread
@ThreadConfined(UI)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ internal class FabricUIManagerBinding : HybridClassBase() {

external fun mergeReactRevision(surfaceId: Int)

external fun getComputedMarginInsets(surfaceId: Int, viewTag: Int): FloatArray?

external fun getComputedPaddingInsets(surfaceId: Int, viewTag: Int): FloatArray?

fun register(
runtimeExecutor: RuntimeExecutor,
runtimeScheduler: RuntimeScheduler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ import android.os.Build
import android.view.View
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.core.graphics.withSave
import com.facebook.react.R
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.fabric.FabricUIManager
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.drawable.BackgroundDrawable
import com.facebook.react.uimanager.drawable.BackgroundImageDrawable
import com.facebook.react.uimanager.drawable.BorderDrawable
Expand All @@ -42,6 +48,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 @@ -440,6 +450,95 @@ public object BackgroundStyleApplicator {
BackgroundStyleApplicator.setBoxShadow(view, shadowStyles)
}

@JvmStatic
public fun setClipPath(view: View, clipPathMap: ReadableMap?) {
val clipPath = ClipPath.parse(clipPathMap)
view.setTag(R.id.clip_path, clipPath)
view.invalidate()
}

@JvmStatic
public fun applyClipPathIfPresent(view: View, canvas: Canvas, drawContent: (() -> Unit?)?) {
val clipPath = view.getTag(R.id.clip_path) as? ClipPath
if (clipPath == null) {
drawContent?.invoke()
return
}

canvas.withSave {
val composite = getCompositeBackgroundDrawable(view)
val uiManager =
UIManagerHelper.getUIManager(view.context as ReactContext, UIManagerType.FABRIC)
as? FabricUIManager
val surfaceId = UIManagerHelper.getSurfaceId(view)
val viewId = view.id
val computedMarginInsets = uiManager?.getComputedMarginInsets(surfaceId, viewId)
val computedPaddingInsets = uiManager?.getComputedPaddingInsets(surfaceId, viewId)
val computedBorderInsets =
composite?.borderInsets?.resolve(composite.layoutDirection, view.context)
val bounds =
getGeometryBoxBounds(
view,
clipPath.geometryBox,
computedMarginInsets,
computedPaddingInsets,
computedBorderInsets,
)
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 borderRadius = composite?.borderRadius
if (borderRadius != null) {
val adjustedBorderRadius =
GeometryBoxUtil.adjustBorderRadiusForGeometryBox(
view,
clipPath.geometryBox,
borderRadius.resolve(
composite.layoutDirection,
view.context,
PixelUtil.toDIPFromPixel(drawingRect.width().toFloat()),
PixelUtil.toDIPFromPixel(drawingRect.height().toFloat()),
),
computedMarginInsets,
computedPaddingInsets,
computedBorderInsets,
)
if (adjustedBorderRadius != null) {
ClipPathUtils.createRoundedRectPath(bounds, adjustedBorderRadius)
} else {
null
}
} else {
null
}
} else {
null
}

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

drawContent?.invoke()
}
}

@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 @@ -133,6 +133,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 @@ -902,6 +903,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 @@ -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 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
Loading
Loading