Skip to content

Commit 4d32b2a

Browse files
authored
Merge pull request #149 from herethesavior/fix/rounded-clickable-press-highlight
fix: round the click ripple to match a clickable view's borderRadius
2 parents 35cfea4 + b531d7b commit 4d32b2a

7 files changed

Lines changed: 98 additions & 18 deletions

File tree

CHANGELOG.md

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

1010
## Fixed
1111

12+
- Match clickable view ripple corners to uniform `borderRadius` on Android 12+.
1213
- Use expedited WorkManager requests for widget background tasks on Android 12+ to reduce delays when the app is in the background, with fallback to regular work when expedited quota is exhausted.
1314

1415
## [0.20.3] - 2026-05-02

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

Lines changed: 14 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,16 @@ private void addClickableArea(RemoteViews widgetView, ViewGroup rootWidget, Clic
179180

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

183+
// The clickable overlay draws its own ripple, so match it to the view's radius when supported
184+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && clickableView.getBorderRadius() > 0f) {
185+
clickableRemoteView.setViewOutlinePreferredRadius(
186+
R.id.rn_widget_clickable_area,
187+
clickableView.getBorderRadius(),
188+
TypedValue.COMPLEX_UNIT_DIP
189+
);
190+
clickableRemoteView.setBoolean(R.id.rn_widget_clickable_area, "setClipToOutline", true);
191+
}
192+
182193
if (clickableView.getAccessibilityLabel() != null) {
183194
clickableRemoteView.setContentDescription(R.id.rn_widget_clickable_area, clickableView.getAccessibilityLabel());
184195
}
@@ -289,6 +300,7 @@ private Intent createCollectionRemoteAdapterIntent(int widgetId, int collectionI
289300
collectionViewMap.putInt("width", offsetViewBounds.width());
290301
collectionViewMap.putString("clickAction", clickableView.getClickAction());
291302
collectionViewMap.putBundle("clickActionData", Arguments.toBundle(clickableView.getClickActionData()));
303+
collectionViewMap.putFloat("borderRadius", clickableView.getBorderRadius());
292304

293305
// Add clickable area accessibility label if available
294306
if (clickableView.getAccessibilityLabel() != null) {
@@ -356,6 +368,7 @@ private WritableMap createClickableAreaPreview(ViewGroup rootView, ClickableView
356368
clickableViewMap.putDouble("height", RNWidgetUtil.pxToDp(appContext, offsetViewBounds.height()));
357369
clickableViewMap.putString("clickAction", clickableView.getClickAction());
358370
clickableViewMap.putMap("clickActionData", Arguments.makeNativeMap(clickableView.getClickActionData().toHashMap()));
371+
clickableViewMap.putDouble("borderRadius", clickableView.getBorderRadius());
359372

360373
return clickableViewMap;
361374
}
@@ -421,6 +434,7 @@ private WritableArray createCollectionItemClickableAreas(CollectionViewItem coll
421434
collectionViewMap.putDouble("height", RNWidgetUtil.pxToDp(appContext, offsetViewBounds.height()));
422435
collectionViewMap.putString("clickAction", clickableView.getClickAction());
423436
collectionViewMap.putMap("clickActionData", Arguments.makeNativeMap(clickableView.getClickActionData().toHashMap()));
437+
collectionViewMap.putDouble("borderRadius", clickableView.getBorderRadius());
424438

425439
clickableAreas.pushMap(collectionViewMap);
426440
}

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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ public class ClickableView implements Comparable<ClickableView> {
1010
private final String clickAction;
1111
private final ReadableMap clickActionData;
1212
private final String accessibilityLabel;
13+
private final float borderRadius;
1314

1415
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData) {
15-
this(id, view, clickAction, clickActionData, null);
16+
this(id, view, clickAction, clickActionData, null, 0f);
1617
}
1718

1819
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData, String accessibilityLabel) {
20+
this(id, view, clickAction, clickActionData, accessibilityLabel, 0f);
21+
}
22+
23+
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData, String accessibilityLabel, float borderRadius) {
1924
this.id = id;
2025
this.view = view;
2126
this.clickAction = clickAction;
2227
this.clickActionData = clickActionData;
2328
this.accessibilityLabel = accessibilityLabel;
29+
this.borderRadius = borderRadius;
2430
}
2531

2632
public String getId() {
@@ -43,6 +49,10 @@ public String getAccessibilityLabel() {
4349
return accessibilityLabel;
4450
}
4551

52+
public float getBorderRadius() {
53+
return borderRadius;
54+
}
55+
4656
@Override
4757
public int compareTo(ClickableView o) {
4858
return this.id.compareTo(o.getId());

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,46 @@ 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");
9798
}
9899
clickableViews.add(
99100
new ClickableView(
100101
id,
101102
view,
102-
config.getMap("props").getString("clickAction"),
103-
config.getMap("props").getMap("clickActionData"),
104-
accessibilityLabel
103+
props.getString("clickAction"),
104+
props.getMap("clickActionData"),
105+
accessibilityLabel,
106+
getUniformBorderRadius(props)
105107
)
106108
);
107109
}
108110

109111
return view;
110112
}
111113

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+
112134
@NonNull
113135
private BaseWidget<? extends View> getBaseWidget(ReactApplicationContext context, ReadableMap config, String id) throws Exception {
114136
switch (Objects.requireNonNull(config.getString("type"))) {

src/api/WidgetPreview.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -258,32 +258,52 @@ function ClickableAreaButton({
258258
highlightClickableAreas,
259259
}: ClickableAreaButtonProps) {
260260
return (
261-
<TouchableNativeFeedback onPress={() => onClick(area)}>
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+
{/* Clip the ripple to the corners: borderRadius + overflow must WRAP the touchable (not sit on
271+
its child), with an explicit bounded Ripple — the default background won't clip (RN #25342). */}
262272
<View
263273
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,
274+
width: '100%',
275+
height: '100%',
276+
borderRadius: area.borderRadius,
277+
overflow: 'hidden',
271278
}}
272279
>
273-
{highlightClickableAreas ? <ClickableAreaBorder /> : null}
280+
<TouchableNativeFeedback
281+
onPress={() => onClick(area)}
282+
background={TouchableNativeFeedback.Ripple('rgba(150, 150, 150, 0.35)', false)}
283+
>
284+
<View
285+
style={{
286+
width: '100%',
287+
height: '100%',
288+
borderColor: 'red',
289+
borderWidth: highlightClickableAreas ? 1 : 0,
290+
}}
291+
/>
292+
</TouchableNativeFeedback>
274293
</View>
275-
</TouchableNativeFeedback>
294+
{highlightClickableAreas ? <ClickableAreaBorder /> : null}
295+
</View>
276296
);
277297
}
278298

279299
function ClickableAreaBorder() {
280300
return (
281-
<>
301+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
282302
<View style={styles.clickableHighlightTopLeft} />
283303
<View style={styles.clickableHighlightTopRight} />
284304
<View style={styles.clickableHighlightBottomLeft} />
285305
<View style={styles.clickableHighlightBottomRight} />
286-
</>
306+
</View>
287307
);
288308
}
289309

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)