Skip to content

Commit

Permalink
add ripple config object to Pressable (#28156)
Browse files Browse the repository at this point in the history
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 bd38686
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 33 deletions.
30 changes: 16 additions & 14 deletions Libraries/Components/Pressable/Pressable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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).
Expand All @@ -138,7 +140,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
const {
accessible,
android_disableSound,
android_rippleColor,
android_ripple,
children,
delayLongPress,
disabled,
Expand All @@ -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);

Expand All @@ -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) {
Expand All @@ -193,7 +195,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
}),
[
android_disableSound,
android_ripple,
android_rippleConfig,
delayLongPress,
disabled,
hitSlop,
Expand All @@ -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}
Expand All @@ -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>,
>);
23 changes: 17 additions & 6 deletions Libraries/Components/Pressable/useAndroidRippleForView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -90,5 +101,5 @@ export default function useAndroidRippleForView(
};
}
return null;
}, [rippleColor, viewRef]);
}, [color, normalizedBorderless, radius, viewRef]);
}
1 change: 1 addition & 0 deletions Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ type AndroidDrawableRipple = $ReadOnly<{|
type: 'RippleAndroid',
color?: ?number,
borderless?: ?boolean,
rippleRadius?: ?number,
|}>;

type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple;
Expand Down
45 changes: 44 additions & 1 deletion RNTester/js/examples/Pressable/PressableExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 11 additions & 9 deletions RNTester/js/examples/Touchable/TouchableExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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 />;
},
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit bd38686

Please sign in to comment.