Skip to content

Commit

Permalink
Set rounded rectangle mask on TouchableNativeFeedback's ripples (#25342)
Browse files Browse the repository at this point in the history
Summary:
This commit fixes an issue where ripple touch feedback extends beyond the border radius of a view.

### Before

<img src="https://user-images.githubusercontent.com/590904/59892832-9cb19180-938f-11e9-8239-b2d5f0e1ce56.png" width="300" />

### After

<img src="https://user-images.githubusercontent.com/590904/59925227-766e0f00-93ec-11e9-9efe-c41e696f8c3c.gif" width="300" />

### The fix

It achieves this by adding a mask to the RippleDrawable background, collecting that information from two new methods on ReactViewGroup:

1. getBorderRadiusMask() returns a drawable rounded rectangle matching the view's border radius properties
2. getBorderRadius() produces a float[] with the border radius information required to build a RoundedRectShape in getBorderRadiusMask()

Additionally, this commit updates setBorderRadius in ReactViewManager to re-apply the background whenever it is set, which is necessary to update the mask on the RippleDrawable background image as the border radius changes.

Related issues:
#6480

## Changelog

[Android][fixed] - Adding border radius styles to TouchableNative react-native run-android --port <x> correctly connects to dev server and related error messages display the correct port
Pull Request resolved: #25342

Test Plan:
Link this branch to a new React native project with the following App.js class:

```
import React, { Component } from "react";
import { StyleSheet, Text, View, TouchableNativeFeedback } from "react-native";

export default class App extends Component {
  render() {
    const ripple = TouchableNativeFeedback.Ripple("#ff0000");

    return (
      <View style={styles.container}>
        <TouchableNativeFeedback background={ripple}>
          <View
            style={{
              width: 96,
              borderRadius: 12,
              borderTopLeftRadius: 10,
              borderBottomRightRadius: 37,
              height: 96,
              alignItems: "center",
              justifyContent: "center",
              borderColor: "black",
              borderWidth: 2
            }}
          >
            <Text>{"CLICK CLICK"}</Text>
          </View>
        </TouchableNativeFeedback>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF"
  }
});
```

It's important to ensure that updates to border radius are accounted for. I did this by enabling hot module reloading and updating the border radius styles to verify that the ripple remains correct.

Reviewed By: cpojer

Differential Revision: D16221213

Pulled By: makovkastar

fbshipit-source-id: 168379591e79f9eca9d184b1607ebb564c2d83dd
  • Loading branch information
nhunzaker authored and facebook-github-bot committed Jul 15, 2019
1 parent 44163b7 commit 14b455f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
Expand All @@ -30,7 +28,8 @@ public class ReactDrawableHelper {

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static Drawable createDrawableFromJSDescription(
Context context, ReadableMap drawableDescriptionDict) {
ReactViewGroup view, ReadableMap drawableDescriptionDict) {
Context context = view.getContext();
String type = drawableDescriptionDict.getString("type");
if ("ThemeAttrAndroid".equals(type)) {
String attr = drawableDescriptionDict.getString("attribute");
Expand Down Expand Up @@ -75,7 +74,7 @@ public static Drawable createDrawableFromJSDescription(
if (!drawableDescriptionDict.hasKey("borderless")
|| drawableDescriptionDict.isNull("borderless")
|| !drawableDescriptionDict.getBoolean("borderless")) {
mask = new ColorDrawable(Color.WHITE);
mask = view.getBorderRadiusMask();
}
ColorStateList colorStateList =
new ColorStateList(new int[][] {new int[] {}}, new int[] {color});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.os.Build;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStructure;
import android.view.animation.Animation;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
Expand Down Expand Up @@ -286,6 +289,31 @@ public void setBorderStyle(@Nullable String style) {
getOrCreateReactViewBackground().setBorderStyle(style);
}

@NonNull
public Drawable getBorderRadiusMask() {
final float[] outerRadii;
if (mReactBackgroundDrawable == null) {
outerRadii = null;
} else {
final float[] borderRadii = getBorderRadii(mReactBackgroundDrawable);
outerRadii =
new float[] {
borderRadii[0],
borderRadii[0],
borderRadii[1],
borderRadii[1],
borderRadii[2],
borderRadii[2],
borderRadii[3],
borderRadii[3]
};
}
final ShapeDrawable shapeDrawable =
new ShapeDrawable(new RoundRectShape(outerRadii, null, null));
shapeDrawable.getPaint().setColor(Color.WHITE);
return shapeDrawable;
}

@Override
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
if (removeClippedSubviews == mRemoveClippedSubviews) {
Expand Down Expand Up @@ -732,92 +760,11 @@ private void dispatchOverflowDraw(Canvas canvas) {
bottom -= borderWidth.bottom;
}

final float borderRadius = mReactBackgroundDrawable.getFullBorderRadius();
float topLeftBorderRadius =
mReactBackgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_LEFT);
float topRightBorderRadius =
mReactBackgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_RIGHT);
float bottomLeftBorderRadius =
mReactBackgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_LEFT);
float bottomRightBorderRadius =
mReactBackgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_RIGHT);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
final boolean isRTL = mLayoutDirection == View.LAYOUT_DIRECTION_RTL;
float topStartBorderRadius =
mReactBackgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_START);
float topEndBorderRadius =
mReactBackgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_END);
float bottomStartBorderRadius =
mReactBackgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_START);
float bottomEndBorderRadius =
mReactBackgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_END);

if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(getContext())) {
if (YogaConstants.isUndefined(topStartBorderRadius)) {
topStartBorderRadius = topLeftBorderRadius;
}

if (YogaConstants.isUndefined(topEndBorderRadius)) {
topEndBorderRadius = topRightBorderRadius;
}

if (YogaConstants.isUndefined(bottomStartBorderRadius)) {
bottomStartBorderRadius = bottomLeftBorderRadius;
}

if (YogaConstants.isUndefined(bottomEndBorderRadius)) {
bottomEndBorderRadius = bottomRightBorderRadius;
}

final float directionAwareTopLeftRadius =
isRTL ? topEndBorderRadius : topStartBorderRadius;
final float directionAwareTopRightRadius =
isRTL ? topStartBorderRadius : topEndBorderRadius;
final float directionAwareBottomLeftRadius =
isRTL ? bottomEndBorderRadius : bottomStartBorderRadius;
final float directionAwareBottomRightRadius =
isRTL ? bottomStartBorderRadius : bottomEndBorderRadius;

topLeftBorderRadius = directionAwareTopLeftRadius;
topRightBorderRadius = directionAwareTopRightRadius;
bottomLeftBorderRadius = directionAwareBottomLeftRadius;
bottomRightBorderRadius = directionAwareBottomRightRadius;
} else {
final float directionAwareTopLeftRadius =
isRTL ? topEndBorderRadius : topStartBorderRadius;
final float directionAwareTopRightRadius =
isRTL ? topStartBorderRadius : topEndBorderRadius;
final float directionAwareBottomLeftRadius =
isRTL ? bottomEndBorderRadius : bottomStartBorderRadius;
final float directionAwareBottomRightRadius =
isRTL ? bottomStartBorderRadius : bottomEndBorderRadius;

if (!YogaConstants.isUndefined(directionAwareTopLeftRadius)) {
topLeftBorderRadius = directionAwareTopLeftRadius;
}

if (!YogaConstants.isUndefined(directionAwareTopRightRadius)) {
topRightBorderRadius = directionAwareTopRightRadius;
}

if (!YogaConstants.isUndefined(directionAwareBottomLeftRadius)) {
bottomLeftBorderRadius = directionAwareBottomLeftRadius;
}

if (!YogaConstants.isUndefined(directionAwareBottomRightRadius)) {
bottomRightBorderRadius = directionAwareBottomRightRadius;
}
}
}
final float borderRadii[] = getBorderRadii(mReactBackgroundDrawable);
final float topLeftBorderRadius = borderRadii[0];
final float topRightBorderRadius = borderRadii[1];
final float bottomRightBorderRadius = borderRadii[2];
final float bottomLeftBorderRadius = borderRadii[3];

if (topLeftBorderRadius > 0
|| topRightBorderRadius > 0
Expand Down Expand Up @@ -856,6 +803,97 @@ private void dispatchOverflowDraw(Canvas canvas) {
}
}

@NonNull
private float[] getBorderRadii(@NonNull ReactViewBackgroundDrawable backgroundDrawable) {
final float borderRadius = backgroundDrawable.getFullBorderRadius();
float topLeftBorderRadius =
backgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_LEFT);
float topRightBorderRadius =
backgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_RIGHT);
float bottomLeftBorderRadius =
backgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_LEFT);
float bottomRightBorderRadius =
backgroundDrawable.getBorderRadiusOrDefaultTo(
borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_RIGHT);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
final boolean isRTL = mLayoutDirection == View.LAYOUT_DIRECTION_RTL;
float topStartBorderRadius =
backgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_START);
float topEndBorderRadius =
backgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_END);
float bottomStartBorderRadius =
backgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_START);
float bottomEndBorderRadius =
backgroundDrawable.getBorderRadius(
ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_END);

if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(getContext())) {
if (YogaConstants.isUndefined(topStartBorderRadius)) {
topStartBorderRadius = topLeftBorderRadius;
}

if (YogaConstants.isUndefined(topEndBorderRadius)) {
topEndBorderRadius = topRightBorderRadius;
}

if (YogaConstants.isUndefined(bottomStartBorderRadius)) {
bottomStartBorderRadius = bottomLeftBorderRadius;
}

if (YogaConstants.isUndefined(bottomEndBorderRadius)) {
bottomEndBorderRadius = bottomRightBorderRadius;
}

final float directionAwareTopLeftRadius = isRTL ? topEndBorderRadius : topStartBorderRadius;
final float directionAwareTopRightRadius =
isRTL ? topStartBorderRadius : topEndBorderRadius;
final float directionAwareBottomLeftRadius =
isRTL ? bottomEndBorderRadius : bottomStartBorderRadius;
final float directionAwareBottomRightRadius =
isRTL ? bottomStartBorderRadius : bottomEndBorderRadius;

topLeftBorderRadius = directionAwareTopLeftRadius;
topRightBorderRadius = directionAwareTopRightRadius;
bottomLeftBorderRadius = directionAwareBottomLeftRadius;
bottomRightBorderRadius = directionAwareBottomRightRadius;
} else {
final float directionAwareTopLeftRadius = isRTL ? topEndBorderRadius : topStartBorderRadius;
final float directionAwareTopRightRadius =
isRTL ? topStartBorderRadius : topEndBorderRadius;
final float directionAwareBottomLeftRadius =
isRTL ? bottomEndBorderRadius : bottomStartBorderRadius;
final float directionAwareBottomRightRadius =
isRTL ? bottomStartBorderRadius : bottomEndBorderRadius;

if (!YogaConstants.isUndefined(directionAwareTopLeftRadius)) {
topLeftBorderRadius = directionAwareTopLeftRadius;
}

if (!YogaConstants.isUndefined(directionAwareTopRightRadius)) {
topRightBorderRadius = directionAwareTopRightRadius;
}

if (!YogaConstants.isUndefined(directionAwareBottomLeftRadius)) {
bottomLeftBorderRadius = directionAwareBottomLeftRadius;
}

if (!YogaConstants.isUndefined(directionAwareBottomRightRadius)) {
bottomRightBorderRadius = directionAwareBottomRightRadius;
}
}
}
return new float[] {
topLeftBorderRadius, topRightBorderRadius, bottomRightBorderRadius, bottomLeftBorderRadius
};
}

public void setOpacityIfPossible(float opacity) {
mBackfaceOpacity = opacity;
setBackfaceVisibilityDependantOpacity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public class ReactViewManager extends ViewGroupManager<ReactViewGroup> {
private static final int CMD_SET_PRESSED = 2;
private static final String HOTSPOT_UPDATE_KEY = "hotspotUpdate";

private @Nullable ReadableMap mNativeBackground;
private @Nullable ReadableMap mNativeForeground;

@ReactProp(name = "accessible")
public void setAccessible(ReactViewGroup view, boolean accessible) {
view.setFocusable(accessible);
Expand Down Expand Up @@ -118,6 +121,14 @@ public void setBorderRadius(ReactViewGroup view, int index, float borderRadius)
} else {
view.setBorderRadius(borderRadius, index - 1);
}

if (mNativeBackground != null) {
setNativeBackground(view, mNativeBackground);
}

if (mNativeForeground != null) {
setNativeForeground(view, mNativeForeground);
}
}

@ReactProp(name = "borderStyle")
Expand Down Expand Up @@ -158,19 +169,17 @@ public void setPointerEvents(ReactViewGroup view, @Nullable String pointerEvents

@ReactProp(name = "nativeBackgroundAndroid")
public void setNativeBackground(ReactViewGroup view, @Nullable ReadableMap bg) {
mNativeBackground = bg;
view.setTranslucentBackgroundDrawable(
bg == null
? null
: ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), bg));
bg == null ? null : ReactDrawableHelper.createDrawableFromJSDescription(view, bg));
}

@TargetApi(Build.VERSION_CODES.M)
@ReactProp(name = "nativeForegroundAndroid")
public void setNativeForeground(ReactViewGroup view, @Nullable ReadableMap fg) {
mNativeForeground = fg;
view.setForeground(
fg == null
? null
: ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), fg));
fg == null ? null : ReactDrawableHelper.createDrawableFromJSDescription(view, fg));
}

@ReactProp(
Expand Down

4 comments on commit 14b455f

@kneza23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, is this included in the latest release 0.60.5?

@mayconmesquita
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, is this included in the latest release 0.60.5?

I think that was included in the v0.61.0-rc.2 release.

@kneza23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can't seem to find it on the list of commits v0.61.0-rc.2...master

@taylorkline
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the tags, it's in v0.61.0-rc.0 and above. You wouldn't see it in the list of commits from v0.61.0-rc.2...master.

Screen Shot 2019-09-19 at 11 20 37 AM

Please sign in to comment.