Skip to content
Permalink
Browse files
add ripple config object to Pressable (#28156)
Summary:
Motivation is to support ripple radius just like in TouchableNativeFeedback, plus borderless attribute. See #28009 (comment)

In the current form this means user needs to pass an `android_ripple` prop which is an object of this shape:
```
export type RippleConfig = {|
  color?: ?ColorValue,
  borderless?: ?boolean,
  radius?: ?number,
|};
```
Do we want to add methods that would create such config objects - https://facebook.github.io/react-native/docs/touchablenativefeedback#methods ?

## Changelog

[Android] [Added] - support borderless and custom ripple radius on Pressable
Pull Request resolved: #28156

Test Plan:
Tested locally in RNTester. I noticed that when some content is rendered after the touchables, the ripple effect is "cut off" by the boundaries of the next view. This is not specific to Pressable, it happens to TouchableNativeFeedback too but I just didn't notice it before in #28009. As it is an issue of its own, I didn't investigate that.

![pressable](https://user-images.githubusercontent.com/1566403/75098762-785f2200-55ba-11ea-8842-e648317610e3.gif)

I changed the Touchable example slightly too (I just moved the "custom ripple radius" up to show the "cutting off" issue), so just for completeness:

![touchable](https://user-images.githubusercontent.com/1566403/75098763-81e88a00-55ba-11ea-9528-e0343d1e054b.gif)

Reviewed By: yungsters

Differential Revision: D20071021

Pulled By: TheSavior

fbshipit-source-id: cb553030934205a52dd50a2a8c8a20da6100e23f
  • Loading branch information
vonovak authored and facebook-github-bot committed Apr 4, 2020
1 parent 2173364 commit bd3868643d29e93610e19312571a9736df2cbdf8
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 33 deletions.
@@ -12,7 +12,9 @@

import * as React from 'react';
import {useMemo, useState, useRef, useImperativeHandle} from 'react';
import useAndroidRippleForView from './useAndroidRippleForView';
import useAndroidRippleForView, {
type RippleConfig,
} from './useAndroidRippleForView';
import type {
AccessibilityActionEvent,
AccessibilityActionInfo,
@@ -122,7 +124,7 @@ type Props = $ReadOnly<{|
/**
* Enables the Android ripple effect and configures its color.
*/
android_rippleColor?: ?ColorValue,
android_ripple?: ?RippleConfig,

/**
* Used only for documentation or testing (e.g. snapshot testing).
@@ -138,7 +140,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
const {
accessible,
android_disableSound,
android_rippleColor,
android_ripple,
children,
delayLongPress,
disabled,
@@ -156,7 +158,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
const viewRef = useRef<React.ElementRef<typeof View> | null>(null);
useImperativeHandle(forwardedRef, () => viewRef.current);

const android_ripple = useAndroidRippleForView(android_rippleColor, viewRef);
const android_rippleConfig = useAndroidRippleForView(android_ripple, viewRef);

const [pressed, setPressed] = usePressState(testOnly_pressed === true);

@@ -172,18 +174,18 @@ function Pressable(props: Props, forwardedRef): React.Node {
onLongPress,
onPress,
onPressIn(event: PressEvent): void {
if (android_ripple != null) {
android_ripple.onPressIn(event);
if (android_rippleConfig != null) {
android_rippleConfig.onPressIn(event);
}
setPressed(true);
if (onPressIn != null) {
onPressIn(event);
}
},
onPressMove: android_ripple?.onPressMove,
onPressMove: android_rippleConfig?.onPressMove,
onPressOut(event: PressEvent): void {
if (android_ripple != null) {
android_ripple.onPressOut(event);
if (android_rippleConfig != null) {
android_rippleConfig.onPressOut(event);
}
setPressed(false);
if (onPressOut != null) {
@@ -193,7 +195,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
}),
[
android_disableSound,
android_ripple,
android_rippleConfig,
delayLongPress,
disabled,
hitSlop,
@@ -211,7 +213,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
<View
{...restProps}
{...eventHandlers}
{...android_ripple?.viewProps}
{...android_rippleConfig?.viewProps}
accessible={accessible !== false}
focusable={focusable !== false}
hitSlop={hitSlop}
@@ -227,10 +229,10 @@ function usePressState(forcePressed: boolean): [boolean, (boolean) => void] {
return [pressed || forcePressed, setPressed];
}

const MemodPressable = React.memo(React.forwardRef(Pressable));
MemodPressable.displayName = 'Pressable';
const MemoedPressable = React.memo(React.forwardRef(Pressable));
MemoedPressable.displayName = 'Pressable';

export default (MemodPressable: React.AbstractComponent<
export default (MemoedPressable: React.AbstractComponent<
Props,
React.ElementRef<typeof View>,
>);
@@ -22,14 +22,21 @@ type NativeBackgroundProp = $ReadOnly<{|
type: 'RippleAndroid',
color: ?number,
borderless: boolean,
rippleRadius: ?number,
|}>;

export type RippleConfig = {|
color?: ?ColorValue,
borderless?: ?boolean,
radius?: ?number,
|};

/**
* Provides the event handlers and props for configuring the ripple effect on
* supported versions of Android.
*/
export default function useAndroidRippleForView(
rippleColor: ?ColorValue,
rippleConfig: ?RippleConfig,
viewRef: {|current: null | React.ElementRef<typeof View>|},
): ?$ReadOnly<{|
onPressIn: (event: PressEvent) => void,
@@ -39,25 +46,29 @@ export default function useAndroidRippleForView(
nativeBackgroundAndroid: NativeBackgroundProp,
|}>,
|}> {
const {color, borderless, radius} = rippleConfig ?? {};
const normalizedBorderless = borderless === true;

return useMemo(() => {
if (
Platform.OS === 'android' &&
Platform.Version >= 21 &&
rippleColor != null
(color != null || normalizedBorderless || radius != null)
) {
const processedColor = processColor(rippleColor);
const processedColor = processColor(color);
invariant(
processedColor == null || typeof processedColor === 'number',
'Unexpected color given for Ripple color',
);

return {
viewProps: {
// Consider supporting `nativeForegroundAndroid` and `borderless`.
// Consider supporting `nativeForegroundAndroid`
nativeBackgroundAndroid: {
type: 'RippleAndroid',
color: processedColor,
borderless: false,
borderless: normalizedBorderless,
rippleRadius: radius,
},
},
onPressIn(event: PressEvent): void {
@@ -90,5 +101,5 @@ export default function useAndroidRippleForView(
};
}
return null;
}, [rippleColor, viewRef]);
}, [color, normalizedBorderless, radius, viewRef]);
}
@@ -230,6 +230,7 @@ type AndroidDrawableRipple = $ReadOnly<{|
type: 'RippleAndroid',
color?: ?number,
borderless?: ?boolean,
rippleRadius?: ?number,
|}>;

type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple;
@@ -369,13 +369,56 @@ exports.examples = [
};
return (
<View style={styles.row}>
<Pressable android_rippleColor="green">
<Pressable android_ripple={{color: 'green'}}>
<Animated.View style={style} />
</Pressable>
</View>
);
},
},
{
title: 'Pressable with custom Ripple',
description: ("Pressable can specify ripple's radius and borderless params": string),
platform: 'android',
render: function(): React.Node {
const nativeFeedbackButton = {
textAlign: 'center',
margin: 10,
};
return (
<View
style={[
styles.row,
{justifyContent: 'space-around', alignItems: 'center'},
]}>
<Pressable
android_ripple={{color: 'orange', borderless: true, radius: 30}}>
<View>
<Text style={[styles.button, nativeFeedbackButton]}>
radius 30
</Text>
</View>
</Pressable>

<Pressable android_ripple={{borderless: true, radius: 150}}>
<View>
<Text style={[styles.button, nativeFeedbackButton]}>
radius 150
</Text>
</View>
</Pressable>

<Pressable android_ripple={{borderless: false, radius: 70}}>
<View style={styles.block}>
<Text style={[styles.button, nativeFeedbackButton]}>
radius 70, with border
</Text>
</View>
</Pressable>
</View>
);
},
},
{
title: '<Text onPress={fn}> with highlight',
render: function(): React.Node {
@@ -452,10 +452,12 @@ function CustomRippleRadius() {

<TouchableNativeFeedback
onPress={() => console.log('custom TNF has been clicked')}
background={TouchableNativeFeedback.SelectableBackgroundBorderless(50)}>
background={TouchableNativeFeedback.SelectableBackgroundBorderless(
150,
)}>
<View>
<Text style={[styles.button, styles.nativeFeedbackButton]}>
radius 50
radius 150
</Text>
</View>
</TouchableNativeFeedback>
@@ -647,18 +649,18 @@ exports.examples = [
},
},
{
title: 'Disabled Touchable*',
description: ('<Touchable*> components accept disabled prop which prevents ' +
'any interaction with component': string),
title: 'Custom Ripple Radius (Android-only)',
description: ('Ripple radius on TouchableNativeFeedback can be controlled': string),
render: function(): React.Element<any> {
return <TouchableDisabled />;
return <CustomRippleRadius />;
},
},
{
title: 'Custom Ripple Radius (Android-only)',
description: ('Ripple radius on TouchableNativeFeedback can be controlled': string),
title: 'Disabled Touchable*',
description: ('<Touchable*> components accept disabled prop which prevents ' +
'any interaction with component': string),
render: function(): React.Element<any> {
return <CustomRippleRadius />;
return <TouchableDisabled />;
},
},
];
@@ -69,7 +69,7 @@ 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");
"Ripple drawable is not available on android API <21");
}
int color = getColor(context, drawableDescriptionDict);
Drawable mask = getMask(drawableDescriptionDict);
@@ -101,7 +101,7 @@ private static int getColor(Context context, ReadableMap drawableDescriptionDict
return context.getResources().getColor(sResolveOutValue.resourceId);
} else {
throw new JSApplicationIllegalArgumentException(
"Attribute colorControlHighlight " + "couldn't be resolved into a drawable");
"Attribute colorControlHighlight couldn't be resolved into a drawable");
}
}
}
@@ -186,7 +186,7 @@ public void setBackground(Drawable drawable) {

public void setTranslucentBackgroundDrawable(@Nullable Drawable background) {
// it's required to call setBackground to null, as in some of the cases we may set new
// background to be a layer drawable that contains a drawable that has been previously setup
// background to be a layer drawable that contains a drawable that has been setup
// as a background previously. This will not work correctly as the drawable callback logic is
// messed up in AOSP
updateBackgroundDrawable(null);

0 comments on commit bd38686

Please sign in to comment.