allow custom ripple radius on TouchableNativeFeedback (#28009)

Summary:
motivation: there are cases where one'd like to control the radius of the ripple effect that's present on TouchableNativeFeedback  - in my case, I want to make sure that both icons and text have the same ripple appearance, but that's currently not possible as far as I can tell.

Currently (afaik) the only way to set  (upper) ripple limits is by specifying width, height and border radius ( + `overflow: hidden`), and this works well for icons which can usually be bounded by a square, but not for text which can have rectangular shape.

This PR adds `rippleRadius` parameter to `SelectableBackground()`, `SelectableBackgroundBorderless()` and `Ripple()` static functions present on `TouchableNativeFeedback`. It can make the ripple smaller but also larger. The result looks like this:

added to RNTester:

![SVID_20200219_182027_1](https://user-images.githubusercontent.com/1566403/74858131-147ff380-5345-11ea-8a9e-2730b79eec38.gif)

difference from the other ripples:

![SVID_20200209_110918_1](https://user-images.githubusercontent.com/1566403/74109152-4513a080-4b81-11ea-8ec3-bb5862c57244.gif)

I'm ofc open to changing the api if needed, but I'm not sure there's much space for manoeuvring. While I was at it, I did a slight refactor of the class into several smaller, more focused methods.

It's possible that in some cases, this might help to work around this issue https://github.com/facebook/react-native/issues/6480.

## Changelog

[Android] [Added] - allow setting custom ripple radius on TouchableNativeFeedback
Pull Request resolved: https://github.com/facebook/react-native/pull/28009

Test Plan: I tested this locally using RNTester

Reviewed By: TheSavior

Differential Revision: D20004509

Pulled By: mdvacca

fbshipit-source-id: 10de1754d54c17878f36a3859705c1188f15c2a2
This commit is contained in:
Vojtech Novak
2020-02-20 15:29:07 -08:00
committed by Facebook Github Bot
parent de8fcfb4cb
commit 7f2a79f40b
3 changed files with 159 additions and 64 deletions
@@ -16,9 +16,13 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.util.TypedValue;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ViewProps;
/**
@@ -41,48 +45,76 @@ public class ReactDrawableHelper {
throw new JSApplicationIllegalArgumentException(
"Attribute " + attr + " couldn't be found in the resource list");
}
if (context.getTheme().resolveAttribute(attrID, sResolveOutValue, true)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return context
.getResources()
.getDrawable(sResolveOutValue.resourceId, context.getTheme());
} else {
return context.getResources().getDrawable(sResolveOutValue.resourceId);
}
} else {
if (!context.getTheme().resolveAttribute(attrID, sResolveOutValue, true)) {
throw new JSApplicationIllegalArgumentException(
"Attribute " + attr + " couldn't be resolved into a drawable");
"Attribute " + attr + " couldn't be resolved into a drawable");
}
Drawable drawable = getDefaultThemeDrawable(context);
return setRadius(drawableDescriptionDict, drawable);
} else if ("RippleAndroid".equals(type)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
throw new JSApplicationIllegalArgumentException(
"Ripple drawable is not available on " + "android API <21");
}
int color;
if (drawableDescriptionDict.hasKey(ViewProps.COLOR)
&& !drawableDescriptionDict.isNull(ViewProps.COLOR)) {
color = drawableDescriptionDict.getInt(ViewProps.COLOR);
} else {
if (context
.getTheme()
.resolveAttribute(android.R.attr.colorControlHighlight, sResolveOutValue, true)) {
color = context.getResources().getColor(sResolveOutValue.resourceId);
} else {
throw new JSApplicationIllegalArgumentException(
"Attribute colorControlHighlight " + "couldn't be resolved into a drawable");
}
}
Drawable mask = null;
if (!drawableDescriptionDict.hasKey("borderless")
|| drawableDescriptionDict.isNull("borderless")
|| !drawableDescriptionDict.getBoolean("borderless")) {
mask = new ColorDrawable(Color.WHITE);
}
ColorStateList colorStateList =
new ColorStateList(new int[][] {new int[] {}}, new int[] {color});
return new RippleDrawable(colorStateList, null, mask);
RippleDrawable rd = getRippleDrawable(context, drawableDescriptionDict);
return setRadius(drawableDescriptionDict, rd);
} else {
throw new JSApplicationIllegalArgumentException("Invalid type for android drawable: " + type);
}
}
private static Drawable getDefaultThemeDrawable(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return context
.getResources()
.getDrawable(sResolveOutValue.resourceId, context.getTheme());
} else {
return context.getResources().getDrawable(sResolveOutValue.resourceId);
}
}
private static RippleDrawable getRippleDrawable(Context context, ReadableMap drawableDescriptionDict) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
throw new JSApplicationIllegalArgumentException(
"Ripple drawable is not available on " + "android API <21");
}
int color = getColor(context, drawableDescriptionDict);
Drawable mask = getMask(drawableDescriptionDict);
ColorStateList colorStateList =
new ColorStateList(new int[][] {new int[] {}}, new int[] {color});
return new RippleDrawable(colorStateList, null, mask);
}
private static Drawable setRadius(ReadableMap drawableDescriptionDict, Drawable drawable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& drawableDescriptionDict.hasKey("rippleRadius")
&& drawable instanceof RippleDrawable) {
RippleDrawable rippleDrawable = (RippleDrawable) drawable;
double rippleRadius = drawableDescriptionDict.getDouble("rippleRadius");
rippleDrawable.setRadius((int) PixelUtil.toPixelFromDIP(rippleRadius));
}
return drawable;
}
private static int getColor(Context context, ReadableMap drawableDescriptionDict) {
if (drawableDescriptionDict.hasKey(ViewProps.COLOR)
&& !drawableDescriptionDict.isNull(ViewProps.COLOR)) {
return drawableDescriptionDict.getInt(ViewProps.COLOR);
} else {
if (context
.getTheme()
.resolveAttribute(android.R.attr.colorControlHighlight, sResolveOutValue, true)) {
return context.getResources().getColor(sResolveOutValue.resourceId);
} else {
throw new JSApplicationIllegalArgumentException(
"Attribute colorControlHighlight " + "couldn't be resolved into a drawable");
}
}
}
private static @Nullable Drawable getMask(ReadableMap drawableDescriptionDict) {
if (!drawableDescriptionDict.hasKey("borderless")
|| drawableDescriptionDict.isNull("borderless")
|| !drawableDescriptionDict.getBoolean("borderless")) {
return new ColorDrawable(Color.WHITE);
}
return null;
}
}