From 14b455f69a30d128db384749347f41b03b9a6000 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 15 Jul 2019 04:11:06 -0700 Subject: [PATCH] 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 ### After ### 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 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 ( {"CLICK CLICK"} ); } } 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 --- .../react/views/view/ReactDrawableHelper.java | 7 +- .../react/views/view/ReactViewGroup.java | 210 +++++++++++------- .../react/views/view/ReactViewManager.java | 21 +- 3 files changed, 142 insertions(+), 96 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java index 8f68c95720..a0d172ad38 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java @@ -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}); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 4cb0e95643..e6b8dcd9bd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -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(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 7acaf8c48a..c1a88cdb24 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -52,6 +52,9 @@ public class ReactViewManager extends ViewGroupManager { 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 { } 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 { @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(