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:
Родитель
de8fcfb4cb
Коммит
7f2a79f40b
|
@ -40,11 +40,13 @@ type Props = $ReadOnly<{|
|
|||
attribute:
|
||||
| 'selectableItemBackground'
|
||||
| 'selectableItemBackgroundBorderless',
|
||||
rippleRadius: ?number,
|
||||
|}>
|
||||
| $ReadOnly<{|
|
||||
type: 'RippleAndroid',
|
||||
color: ?number,
|
||||
borderless: boolean,
|
||||
rippleRadius: ?number,
|
||||
|}>
|
||||
),
|
||||
|
||||
|
@ -100,24 +102,32 @@ class TouchableNativeFeedback extends React.Component<Props, State> {
|
|||
* Creates a value for the `background` prop that uses the Android theme's
|
||||
* default background for selectable elements.
|
||||
*/
|
||||
static SelectableBackground: () => $ReadOnly<{|
|
||||
static SelectableBackground: (
|
||||
rippleRadius: ?number,
|
||||
) => $ReadOnly<{|
|
||||
attribute: 'selectableItemBackground',
|
||||
type: 'ThemeAttrAndroid',
|
||||
|}> = () => ({
|
||||
rippleRadius: ?number,
|
||||
|}> = (rippleRadius: ?number) => ({
|
||||
type: 'ThemeAttrAndroid',
|
||||
attribute: 'selectableItemBackground',
|
||||
rippleRadius,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a value for the `background` prop that uses the Android theme's
|
||||
* default background for borderless selectable elements. Requires API 21+.
|
||||
*/
|
||||
static SelectableBackgroundBorderless: () => $ReadOnly<{|
|
||||
static SelectableBackgroundBorderless: (
|
||||
rippleRadius: ?number,
|
||||
) => $ReadOnly<{|
|
||||
attribute: 'selectableItemBackgroundBorderless',
|
||||
type: 'ThemeAttrAndroid',
|
||||
|}> = () => ({
|
||||
rippleRadius: ?number,
|
||||
|}> = (rippleRadius: ?number) => ({
|
||||
type: 'ThemeAttrAndroid',
|
||||
attribute: 'selectableItemBackgroundBorderless',
|
||||
rippleRadius,
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -128,11 +138,13 @@ class TouchableNativeFeedback extends React.Component<Props, State> {
|
|||
static Ripple: (
|
||||
color: string,
|
||||
borderless: boolean,
|
||||
rippleRadius: ?number,
|
||||
) => $ReadOnly<{|
|
||||
borderless: boolean,
|
||||
color: ?number,
|
||||
rippleRadius: ?number,
|
||||
type: 'RippleAndroid',
|
||||
|}> = (color: string, borderless: boolean) => {
|
||||
|}> = (color: string, borderless: boolean, rippleRadius: ?number) => {
|
||||
const processedColor = processColor(color);
|
||||
invariant(
|
||||
processedColor == null || typeof processedColor === 'number',
|
||||
|
@ -142,6 +154,7 @@ class TouchableNativeFeedback extends React.Component<Props, State> {
|
|||
type: 'RippleAndroid',
|
||||
color: processedColor,
|
||||
borderless,
|
||||
rippleRadius,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -401,35 +401,78 @@ class TouchableDisabled extends React.Component<{...}> {
|
|||
</TouchableWithoutFeedback>
|
||||
|
||||
{Platform.OS === 'android' && (
|
||||
<TouchableNativeFeedback
|
||||
onPress={() => console.log('custom TNF has been clicked')}
|
||||
background={TouchableNativeFeedback.SelectableBackground()}>
|
||||
<View style={[styles.row, styles.block]}>
|
||||
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
||||
Enabled TouchableNativeFeedback
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
)}
|
||||
<>
|
||||
<TouchableNativeFeedback
|
||||
onPress={() => console.log('custom TNF has been clicked')}
|
||||
background={TouchableNativeFeedback.SelectableBackground()}>
|
||||
<View style={[styles.row, styles.block]}>
|
||||
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
||||
Enabled TouchableNativeFeedback
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
|
||||
{Platform.OS === 'android' && (
|
||||
<TouchableNativeFeedback
|
||||
disabled={true}
|
||||
onPress={() => console.log('custom TNF has been clicked')}
|
||||
background={TouchableNativeFeedback.SelectableBackground()}>
|
||||
<View style={[styles.row, styles.block]}>
|
||||
<Text
|
||||
style={[styles.disabledButton, styles.nativeFeedbackButton]}>
|
||||
Disabled TouchableNativeFeedback
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
<TouchableNativeFeedback
|
||||
disabled={true}
|
||||
onPress={() => console.log('custom TNF has been clicked')}
|
||||
background={TouchableNativeFeedback.SelectableBackground()}>
|
||||
<View style={[styles.row, styles.block]}>
|
||||
<Text
|
||||
style={[styles.disabledButton, styles.nativeFeedbackButton]}>
|
||||
Disabled TouchableNativeFeedback
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function CustomRippleRadius() {
|
||||
if (Platform.OS !== 'android') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.row,
|
||||
{justifyContent: 'space-around', alignItems: 'center'},
|
||||
]}>
|
||||
<TouchableNativeFeedback
|
||||
onPress={() => console.log('custom TNF has been clicked')}
|
||||
background={TouchableNativeFeedback.Ripple('orange', true, 30)}>
|
||||
<View>
|
||||
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
||||
radius 30
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
|
||||
<TouchableNativeFeedback
|
||||
onPress={() => console.log('custom TNF has been clicked')}
|
||||
background={TouchableNativeFeedback.SelectableBackgroundBorderless(50)}>
|
||||
<View>
|
||||
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
||||
radius 50
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
|
||||
<TouchableNativeFeedback
|
||||
onPress={() => console.log('custom TNF has been clicked')}
|
||||
background={TouchableNativeFeedback.SelectableBackground(70)}>
|
||||
<View style={styles.block}>
|
||||
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
||||
radius 70, with border
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const remoteImage = {
|
||||
uri: 'https://www.facebook.com/favicon.ico',
|
||||
};
|
||||
|
@ -611,4 +654,11 @@ exports.examples = [
|
|||
return <TouchableDisabled />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Custom Ripple Radius (Android-only)',
|
||||
description: ('Ripple radius on TouchableNativeFeedback can be controlled': string),
|
||||
render: function(): React.Element<any> {
|
||||
return <CustomRippleRadius />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче