Set rounded rectangle mask on TouchableNativeFeedback's ripples (#25342)

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:
https://github.com/facebook/react-native/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: https://github.com/facebook/react-native/pull/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
This commit is contained in:
Nate 2019-07-15 04:11:06 -07:00 коммит произвёл Facebook Github Bot
Родитель 44163b750f
Коммит 14b455f69a
3 изменённых файлов: 142 добавлений и 96 удалений

Просмотреть файл

@ -9,8 +9,6 @@ package com.facebook.react.views.view;
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;
@ -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");
@ -75,7 +74,7 @@ public class ReactDrawableHelper {
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});

Просмотреть файл

@ -18,12 +18,15 @@ import android.graphics.Rect;
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;
@ -286,6 +289,31 @@ public class ReactViewGroup extends ViewGroup
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) {
@ -732,92 +760,11 @@ public class ReactViewGroup extends ViewGroup
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
@ -856,6 +803,97 @@ public class ReactViewGroup extends ViewGroup
}
}
@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();

Просмотреть файл

@ -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);
@ -118,6 +121,14 @@ public class ReactViewManager extends ViewGroupManager<ReactViewGroup> {
} else {
view.setBorderRadius(borderRadius, index - 1);
}
if (mNativeBackground != null) {
setNativeBackground(view, mNativeBackground);
}
if (mNativeForeground != null) {
setNativeForeground(view, mNativeForeground);
}
}
@ReactProp(name = "borderStyle")
@ -158,19 +169,17 @@ public class ReactViewManager extends ViewGroupManager<ReactViewGroup> {
@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(