Skip to content

Commit bb88f6e

Browse files
committed
fix: round the click ripple to match a clickable view's borderRadius
The press highlight is rendered on a separate transparent overlay (?attr/selectableItemBackground) laid over each clickable view's bounds, independently of the rendered widget bitmap. Because that overlay is a plain rectangle, a rounded button or card showed a rectangular ripple on press that didn't match its shape. Plumb each clickable view's borderRadius (WidgetFactory -> ClickableView) and, on API 31+, clip the overlay's outline to it with setViewOutlinePreferredRadius so the ripple follows the view's corners. Touch bounds are unchanged (the outline only clips drawing, so corners stay tappable), views with square corners are untouched, and below API 31 the highlight stays rectangular.
1 parent 9cbfed9 commit bb88f6e

4 files changed

Lines changed: 51 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## Fixed
11+
12+
- The press highlight (ripple) of a clickable view is drawn on a separate overlay, so it ignored the view's `borderRadius` and always appeared as a rectangle over rounded buttons/cards. It now follows the clickable view's `borderRadius` on Android 12+ (API 31+) via `setViewOutlinePreferredRadius`; views with square corners and older versions are unchanged.
13+
1014
## [0.20.3] - 2026-05-02
1115

1216
## Fixed

android/src/main/java/com/reactnativeandroidwidget/RNWidget.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.os.Build;
1212
import android.os.Bundle;
1313
import android.util.Base64;
14+
import android.util.TypedValue;
1415
import android.view.View;
1516
import android.view.ViewGroup;
1617
import android.widget.RemoteViews;
@@ -179,6 +180,22 @@ private void addClickableArea(RemoteViews widgetView, ViewGroup rootWidget, Clic
179180

180181
clickableRemoteView.setInt(R.id.rn_widget_clickable_area, "setMinimumHeight", offsetViewBounds.height());
181182

183+
// Round the press-highlight to match the clickable view's own corner radius. The ripple lives
184+
// on THIS separate overlay (rn_widget_clickable_area, background = ?attr/selectableItemBackground)
185+
// laid over the view's bounds — it is NOT the rendered widget bitmap, so a rounded view (e.g. a
186+
// pill-shaped button or a card with a borderRadius) otherwise gets a RECTANGULAR highlight on
187+
// press. setViewOutlinePreferredRadius (API 31+) clips the overlay — and, via clipToOutline, its
188+
// ripple — to a rounded outline that matches the view. Touch bounds are unaffected (the outline
189+
// only clips drawing), so the corners stay tappable. Views with square corners (radius 0) are
190+
// left untouched, and below API 31 the highlight remains rectangular.
191+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && clickableView.getBorderRadius() > 0f) {
192+
clickableRemoteView.setViewOutlinePreferredRadius(
193+
R.id.rn_widget_clickable_area,
194+
clickableView.getBorderRadius(),
195+
TypedValue.COMPLEX_UNIT_DIP
196+
);
197+
}
198+
182199
if (clickableView.getAccessibilityLabel() != null) {
183200
clickableRemoteView.setContentDescription(R.id.rn_widget_clickable_area, clickableView.getAccessibilityLabel());
184201
}

android/src/main/java/com/reactnativeandroidwidget/builder/ClickableView.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,25 @@ public class ClickableView implements Comparable<ClickableView> {
1010
private final String clickAction;
1111
private final ReadableMap clickActionData;
1212
private final String accessibilityLabel;
13+
// Corner radius (dp) of the clickable view, so the press-highlight overlay can be rounded to match
14+
// it (see RNWidget#addClickableArea). 0 = square corners (the default / previous behavior).
15+
private final float borderRadius;
1316

1417
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData) {
15-
this(id, view, clickAction, clickActionData, null);
18+
this(id, view, clickAction, clickActionData, null, 0f);
1619
}
1720

1821
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData, String accessibilityLabel) {
22+
this(id, view, clickAction, clickActionData, accessibilityLabel, 0f);
23+
}
24+
25+
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData, String accessibilityLabel, float borderRadius) {
1926
this.id = id;
2027
this.view = view;
2128
this.clickAction = clickAction;
2229
this.clickActionData = clickActionData;
2330
this.accessibilityLabel = accessibilityLabel;
31+
this.borderRadius = borderRadius;
2432
}
2533

2634
public String getId() {
@@ -43,6 +51,10 @@ public String getAccessibilityLabel() {
4351
return accessibilityLabel;
4452
}
4553

54+
public float getBorderRadius() {
55+
return borderRadius;
56+
}
57+
4658
@Override
4759
public int compareTo(ClickableView o) {
4860
return this.id.compareTo(o.getId());

android/src/main/java/com/reactnativeandroidwidget/builder/WidgetFactory.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,29 @@ private View buildWidget(ReactApplicationContext context, ReadableMap config, St
9191
}
9292

9393
if (config.getMap("props").hasKey("clickAction")) {
94+
ReadableMap props = config.getMap("props");
9495
String accessibilityLabel = null;
95-
if (config.getMap("props").hasKey("accessibilityLabel")) {
96-
accessibilityLabel = config.getMap("props").getString("accessibilityLabel");
96+
if (props.hasKey("accessibilityLabel")) {
97+
accessibilityLabel = props.getString("accessibilityLabel");
98+
}
99+
// Carry the view's corner radius (dp) so its press-highlight overlay can be rounded to
100+
// match (see RNWidget#addClickableArea). borderRadius is serialised as a per-corner map;
101+
// widgets typically use a uniform radius, so topLeft represents the shape. Absent/null → 0.
102+
float clickBorderRadius = 0f;
103+
if (props.hasKey("borderRadius") && !props.isNull("borderRadius")) {
104+
ReadableMap borderRadius = props.getMap("borderRadius");
105+
if (borderRadius != null && borderRadius.hasKey("topLeft")) {
106+
clickBorderRadius = (float) borderRadius.getDouble("topLeft");
107+
}
97108
}
98109
clickableViews.add(
99110
new ClickableView(
100111
id,
101112
view,
102-
config.getMap("props").getString("clickAction"),
103-
config.getMap("props").getMap("clickActionData"),
104-
accessibilityLabel
113+
props.getString("clickAction"),
114+
props.getMap("clickActionData"),
115+
accessibilityLabel,
116+
clickBorderRadius
105117
)
106118
);
107119
}

0 commit comments

Comments
 (0)