Skip to content

Commit d099401

Browse files
committed
fix: round ripple only for uniform borderRadius, and support lists + preview
Address PR review feedback: - Only round the press highlight when all four corners share the same radius, via a new getUniformBorderRadius helper; non-uniform radii fall back to the rectangular highlight instead of assuming topLeft. - Apply the same rounding to collection (list) item clickable areas in RNWidgetCollectionService, plumbing borderRadius through the item data. - Round the ripple in WidgetPreview so it matches the on-device result while working on the widget. - Simplify/remove comments per review.
1 parent f82b9af commit d099401

7 files changed

Lines changed: 67 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## Fixed
1111

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.
12+
- Match clickable view ripple corners to uniform `borderRadius` on Android 12+.
1313

1414
## [0.20.3] - 2026-05-02
1515

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,7 @@ private void addClickableArea(RemoteViews widgetView, ViewGroup rootWidget, Clic
180180

181181
clickableRemoteView.setInt(R.id.rn_widget_clickable_area, "setMinimumHeight", offsetViewBounds.height());
182182

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. Rounding needs BOTH calls (API 31+): setViewOutlinePreferredRadius gives the overlay a
188-
// round-rect OutlineProvider, and setClipToOutline=true makes the view actually clip its drawing
189-
// (including the ripple background) to that outline — an outline on its own does not clip. This
190-
// mirrors AndroidX RemoteViewsCompat (setViewOutlinePreferredRadius + setViewClipToOutline, both
191-
// @RequiresApi(31)); setBoolean invokes View#setClipToOutline directly, avoiding a new dependency.
192-
// Touch bounds are unaffected (the outline only clips drawing), so the corners stay tappable.
193-
// Views with square corners (radius 0) are left untouched, and below API 31 the highlight remains
194-
// rectangular.
183+
// The clickable overlay draws its own ripple, so match it to the view's radius when supported
195184
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && clickableView.getBorderRadius() > 0f) {
196185
clickableRemoteView.setViewOutlinePreferredRadius(
197186
R.id.rn_widget_clickable_area,
@@ -311,6 +300,7 @@ private Intent createCollectionRemoteAdapterIntent(int widgetId, int collectionI
311300
collectionViewMap.putInt("width", offsetViewBounds.width());
312301
collectionViewMap.putString("clickAction", clickableView.getClickAction());
313302
collectionViewMap.putBundle("clickActionData", Arguments.toBundle(clickableView.getClickActionData()));
303+
collectionViewMap.putFloat("borderRadius", clickableView.getBorderRadius());
314304

315305
// Add clickable area accessibility label if available
316306
if (clickableView.getAccessibilityLabel() != null) {
@@ -378,6 +368,7 @@ private WritableMap createClickableAreaPreview(ViewGroup rootView, ClickableView
378368
clickableViewMap.putDouble("height", RNWidgetUtil.pxToDp(appContext, offsetViewBounds.height()));
379369
clickableViewMap.putString("clickAction", clickableView.getClickAction());
380370
clickableViewMap.putMap("clickActionData", Arguments.makeNativeMap(clickableView.getClickActionData().toHashMap()));
371+
clickableViewMap.putDouble("borderRadius", clickableView.getBorderRadius());
381372

382373
return clickableViewMap;
383374
}
@@ -443,6 +434,7 @@ private WritableArray createCollectionItemClickableAreas(CollectionViewItem coll
443434
collectionViewMap.putDouble("height", RNWidgetUtil.pxToDp(appContext, offsetViewBounds.height()));
444435
collectionViewMap.putString("clickAction", clickableView.getClickAction());
445436
collectionViewMap.putMap("clickActionData", Arguments.makeNativeMap(clickableView.getClickActionData().toHashMap()));
437+
collectionViewMap.putDouble("borderRadius", clickableView.getBorderRadius());
446438

447439
clickableAreas.pushMap(collectionViewMap);
448440
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.net.Uri;
99
import android.os.Build;
1010
import android.os.Bundle;
11+
import android.util.TypedValue;
1112
import android.widget.RemoteViews;
1213
import android.widget.RemoteViewsService;
1314

@@ -116,6 +117,17 @@ private void addClickableArea(RemoteViews widgetView, Bundle clickableArea, int
116117

117118
clickableRemoteView.setInt(R.id.rn_widget_clickable_area, "setMinimumHeight", clickableArea.getInt("height"));
118119

120+
// The clickable overlay draws its own ripple, so match it to the view's radius when supported
121+
float borderRadius = clickableArea.getFloat("borderRadius", 0f);
122+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && borderRadius > 0f) {
123+
clickableRemoteView.setViewOutlinePreferredRadius(
124+
R.id.rn_widget_clickable_area,
125+
borderRadius,
126+
TypedValue.COMPLEX_UNIT_DIP
127+
);
128+
clickableRemoteView.setBoolean(R.id.rn_widget_clickable_area, "setClipToOutline", true);
129+
}
130+
119131
// Set accessibility label on clickable area if available
120132
if (clickableArea.getString("accessibilityLabel", null) != null) {
121133
clickableRemoteView.setContentDescription(R.id.rn_widget_clickable_area, clickableArea.getString("accessibilityLabel"));

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ 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).
1513
private final float borderRadius;
1614

1715
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData) {

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

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,31 +96,41 @@ private View buildWidget(ReactApplicationContext context, ReadableMap config, St
9696
if (props.hasKey("accessibilityLabel")) {
9797
accessibilityLabel = props.getString("accessibilityLabel");
9898
}
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-
}
108-
}
10999
clickableViews.add(
110100
new ClickableView(
111101
id,
112102
view,
113103
props.getString("clickAction"),
114104
props.getMap("clickActionData"),
115105
accessibilityLabel,
116-
clickBorderRadius
106+
getUniformBorderRadius(props)
117107
)
118108
);
119109
}
120110

121111
return view;
122112
}
123113

114+
private float getUniformBorderRadius(ReadableMap props) {
115+
if (!props.hasKey("borderRadius") || props.isNull("borderRadius")) {
116+
return 0f;
117+
}
118+
119+
ReadableMap radius = props.getMap("borderRadius");
120+
if (radius == null) {
121+
return 0f;
122+
}
123+
124+
double topLeft = radius.hasKey("topLeft") ? radius.getDouble("topLeft") : 0;
125+
double topRight = radius.hasKey("topRight") ? radius.getDouble("topRight") : 0;
126+
double bottomRight = radius.hasKey("bottomRight") ? radius.getDouble("bottomRight") : 0;
127+
double bottomLeft = radius.hasKey("bottomLeft") ? radius.getDouble("bottomLeft") : 0;
128+
129+
return topLeft == topRight && topLeft == bottomRight && topLeft == bottomLeft
130+
? (float) topLeft
131+
: 0f;
132+
}
133+
124134
@NonNull
125135
private BaseWidget<? extends View> getBaseWidget(ReactApplicationContext context, ReadableMap config, String id) throws Exception {
126136
switch (Objects.requireNonNull(config.getString("type"))) {

src/api/WidgetPreview.tsx

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -258,32 +258,43 @@ function ClickableAreaButton({
258258
highlightClickableAreas,
259259
}: ClickableAreaButtonProps) {
260260
return (
261-
<TouchableNativeFeedback onPress={() => onClick(area)}>
262-
<View
263-
style={{
264-
position: 'absolute',
265-
left: area.left,
266-
top: area.top,
267-
width: area.width,
268-
height: area.height,
269-
borderColor: 'red',
270-
borderWidth: highlightClickableAreas ? 1 : 0,
271-
}}
272-
>
273-
{highlightClickableAreas ? <ClickableAreaBorder /> : null}
274-
</View>
275-
</TouchableNativeFeedback>
261+
<View
262+
style={{
263+
position: 'absolute',
264+
left: area.left,
265+
top: area.top,
266+
width: area.width,
267+
height: area.height,
268+
}}
269+
>
270+
<TouchableNativeFeedback onPress={() => onClick(area)}>
271+
{/* borderRadius + overflow clip the press ripple to the view's rounded corners so the
272+
preview matches the on-device rounded ripple (see RNWidget#addClickableArea). The
273+
highlight markers live on the parent so they are not clipped. */}
274+
<View
275+
style={{
276+
width: '100%',
277+
height: '100%',
278+
borderColor: 'red',
279+
borderWidth: highlightClickableAreas ? 1 : 0,
280+
borderRadius: area.borderRadius,
281+
overflow: 'hidden',
282+
}}
283+
/>
284+
</TouchableNativeFeedback>
285+
{highlightClickableAreas ? <ClickableAreaBorder /> : null}
286+
</View>
276287
);
277288
}
278289

279290
function ClickableAreaBorder() {
280291
return (
281-
<>
292+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
282293
<View style={styles.clickableHighlightTopLeft} />
283294
<View style={styles.clickableHighlightTopRight} />
284295
<View style={styles.clickableHighlightBottomLeft} />
285296
<View style={styles.clickableHighlightBottomRight} />
286-
</>
297+
</View>
287298
);
288299
}
289300

src/api/private.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ClickableArea extends OnClick {
88
top: number;
99
width: number;
1010
height: number;
11+
borderRadius: number;
1112
}
1213

1314
export interface CollectionItem extends OnClick {

0 commit comments

Comments
 (0)