mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
bd3868643d
Summary: Motivation is to support ripple radius just like in TouchableNativeFeedback, plus borderless attribute. See https://github.com/facebook/react-native/pull/28009#issuecomment-589489520 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: https://github.com/facebook/react-native/pull/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 https://github.com/facebook/react-native/pull/28009. As it is an issue of its own, I didn't investigate that.  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:  Reviewed By: yungsters Differential Revision: D20071021 Pulled By: TheSavior fbshipit-source-id: cb553030934205a52dd50a2a8c8a20da6100e23f
667 lines
18 KiB
JavaScript
667 lines
18 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @flow
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const React = require('react');
|
|
|
|
const {
|
|
Animated,
|
|
Image,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableHighlight,
|
|
TouchableOpacity,
|
|
Platform,
|
|
TouchableNativeFeedback,
|
|
TouchableWithoutFeedback,
|
|
View,
|
|
} = require('react-native');
|
|
|
|
import {useEffect, useRef, useState} from 'react';
|
|
|
|
const forceTouchAvailable =
|
|
(Platform.OS === 'ios' && Platform.constants.forceTouchAvailable) || false;
|
|
|
|
class TouchableHighlightBox extends React.Component<{...}, $FlowFixMeState> {
|
|
state = {
|
|
timesPressed: 0,
|
|
};
|
|
|
|
touchableOnPress = () => {
|
|
this.setState({
|
|
timesPressed: this.state.timesPressed + 1,
|
|
});
|
|
};
|
|
|
|
render() {
|
|
let textLog = '';
|
|
if (this.state.timesPressed > 1) {
|
|
textLog = this.state.timesPressed + 'x TouchableHighlight onPress';
|
|
} else if (this.state.timesPressed > 0) {
|
|
textLog = 'TouchableHighlight onPress';
|
|
}
|
|
|
|
return (
|
|
<View>
|
|
<View style={styles.row}>
|
|
<TouchableHighlight
|
|
style={styles.wrapper}
|
|
testID="touchable_highlight_image_button"
|
|
onPress={this.touchableOnPress}>
|
|
<Image source={remoteImage} style={styles.image} />
|
|
</TouchableHighlight>
|
|
<TouchableHighlight
|
|
style={styles.wrapper}
|
|
testID="touchable_highlight_text_button"
|
|
activeOpacity={1}
|
|
underlayColor="rgb(210, 230, 255)"
|
|
onPress={this.touchableOnPress}>
|
|
<View style={styles.wrapperCustom}>
|
|
<Text style={styles.text}>Tap Here For Custom Highlight!</Text>
|
|
</View>
|
|
</TouchableHighlight>
|
|
</View>
|
|
<View style={styles.logBox}>
|
|
<Text testID="touchable_highlight_console">{textLog}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
class TouchableWithoutFeedbackBox extends React.Component<
|
|
{...},
|
|
$FlowFixMeState,
|
|
> {
|
|
state = {
|
|
timesPressed: 0,
|
|
};
|
|
|
|
textOnPress = () => {
|
|
this.setState({
|
|
timesPressed: this.state.timesPressed + 1,
|
|
});
|
|
};
|
|
|
|
render() {
|
|
let textLog = '';
|
|
if (this.state.timesPressed > 1) {
|
|
textLog = this.state.timesPressed + 'x TouchableWithoutFeedback onPress';
|
|
} else if (this.state.timesPressed > 0) {
|
|
textLog = 'TouchableWithoutFeedback onPress';
|
|
}
|
|
|
|
return (
|
|
<View>
|
|
<TouchableWithoutFeedback
|
|
onPress={this.textOnPress}
|
|
testID="touchable_without_feedback_button">
|
|
<View style={styles.wrapperCustom}>
|
|
<Text style={styles.text}>Tap Here For No Feedback!</Text>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
<View style={styles.logBox}>
|
|
<Text testID="touchable_without_feedback_console">{textLog}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
class TextOnPressBox extends React.Component<{...}, $FlowFixMeState> {
|
|
state = {
|
|
timesPressed: 0,
|
|
};
|
|
|
|
textOnPress = () => {
|
|
this.setState({
|
|
timesPressed: this.state.timesPressed + 1,
|
|
});
|
|
};
|
|
|
|
render() {
|
|
let textLog = '';
|
|
if (this.state.timesPressed > 1) {
|
|
textLog = this.state.timesPressed + 'x text onPress';
|
|
} else if (this.state.timesPressed > 0) {
|
|
textLog = 'text onPress';
|
|
}
|
|
|
|
return (
|
|
<View>
|
|
<Text
|
|
style={styles.textBlock}
|
|
testID="tappable_text"
|
|
onPress={this.textOnPress}>
|
|
Text has built-in onPress handling
|
|
</Text>
|
|
<View style={styles.logBox}>
|
|
<Text testID="tappable_text_console">{textLog}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
class TouchableFeedbackEvents extends React.Component<{...}, $FlowFixMeState> {
|
|
state = {
|
|
eventLog: [],
|
|
};
|
|
|
|
render() {
|
|
return (
|
|
<View testID="touchable_feedback_events">
|
|
<View style={[styles.row, styles.centered]}>
|
|
<TouchableOpacity
|
|
style={styles.wrapper}
|
|
testID="touchable_feedback_events_button"
|
|
accessibilityLabel="touchable feedback events"
|
|
accessibilityRole="button"
|
|
onPress={() => this._appendEvent('press')}
|
|
onPressIn={() => this._appendEvent('pressIn')}
|
|
onPressOut={() => this._appendEvent('pressOut')}
|
|
onLongPress={() => this._appendEvent('longPress')}>
|
|
<Text style={styles.button}>Press Me</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View
|
|
testID="touchable_feedback_events_console"
|
|
style={styles.eventLogBox}>
|
|
{this.state.eventLog.map((e, ii) => (
|
|
<Text key={ii}>{e}</Text>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
_appendEvent = eventName => {
|
|
const limit = 6;
|
|
const eventLog = this.state.eventLog.slice(0, limit - 1);
|
|
eventLog.unshift(eventName);
|
|
this.setState({eventLog});
|
|
};
|
|
}
|
|
|
|
class TouchableDelayEvents extends React.Component<{...}, $FlowFixMeState> {
|
|
state = {
|
|
eventLog: [],
|
|
};
|
|
|
|
render() {
|
|
return (
|
|
<View testID="touchable_delay_events">
|
|
<View style={[styles.row, styles.centered]}>
|
|
<TouchableOpacity
|
|
style={styles.wrapper}
|
|
testID="touchable_delay_events_button"
|
|
onPress={() => this._appendEvent('press')}
|
|
delayPressIn={400}
|
|
onPressIn={() => this._appendEvent('pressIn - 400ms delay')}
|
|
delayPressOut={1000}
|
|
onPressOut={() => this._appendEvent('pressOut - 1000ms delay')}
|
|
delayLongPress={800}
|
|
onLongPress={() => this._appendEvent('longPress - 800ms delay')}>
|
|
<Text style={styles.button}>Press Me</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View
|
|
style={styles.eventLogBox}
|
|
testID="touchable_delay_events_console">
|
|
{this.state.eventLog.map((e, ii) => (
|
|
<Text key={ii}>{e}</Text>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
_appendEvent = eventName => {
|
|
const limit = 6;
|
|
const eventLog = this.state.eventLog.slice(0, limit - 1);
|
|
eventLog.unshift(eventName);
|
|
this.setState({eventLog});
|
|
};
|
|
}
|
|
|
|
class ForceTouchExample extends React.Component<{...}, $FlowFixMeState> {
|
|
state = {
|
|
force: 0,
|
|
};
|
|
|
|
_renderConsoleText = () => {
|
|
return forceTouchAvailable
|
|
? 'Force: ' + this.state.force.toFixed(3)
|
|
: '3D Touch is not available on this device';
|
|
};
|
|
|
|
render() {
|
|
return (
|
|
<View testID="touchable_3dtouch_event">
|
|
<View style={styles.forceTouchBox} testID="touchable_3dtouch_output">
|
|
<Text>{this._renderConsoleText()}</Text>
|
|
</View>
|
|
<View style={[styles.row, styles.centered]}>
|
|
<View
|
|
style={styles.wrapper}
|
|
testID="touchable_3dtouch_button"
|
|
onStartShouldSetResponder={() => true}
|
|
onResponderMove={event =>
|
|
this.setState({force: event.nativeEvent.force})
|
|
}
|
|
onResponderRelease={event => this.setState({force: 0})}>
|
|
<Text style={styles.button}>Press Me</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
class TouchableHitSlop extends React.Component<{...}, $FlowFixMeState> {
|
|
state = {
|
|
timesPressed: 0,
|
|
};
|
|
|
|
onPress = () => {
|
|
this.setState({
|
|
timesPressed: this.state.timesPressed + 1,
|
|
});
|
|
};
|
|
|
|
render() {
|
|
let log = '';
|
|
if (this.state.timesPressed > 1) {
|
|
log = this.state.timesPressed + 'x onPress';
|
|
} else if (this.state.timesPressed > 0) {
|
|
log = 'onPress';
|
|
}
|
|
|
|
return (
|
|
<View testID="touchable_hit_slop">
|
|
<View style={[styles.row, styles.centered]}>
|
|
<TouchableOpacity
|
|
onPress={this.onPress}
|
|
style={styles.hitSlopWrapper}
|
|
hitSlop={{top: 30, bottom: 30, left: 60, right: 60}}
|
|
testID="touchable_hit_slop_button">
|
|
<Text style={styles.hitSlopButton}>Press Outside This View</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View style={styles.logBox}>
|
|
<Text>{log}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
function TouchableNativeMethodChecker<
|
|
T: React.AbstractComponent<any, any>,
|
|
>(props: {|Component: T, name: string|}): React.Node {
|
|
const [status, setStatus] = useState<?boolean>(null);
|
|
const ref = useRef<?React.ElementRef<T>>(null);
|
|
|
|
useEffect(() => {
|
|
setStatus(ref.current != null && typeof ref.current.measure === 'function');
|
|
}, []);
|
|
|
|
return (
|
|
<View style={[styles.row, styles.block]}>
|
|
<props.Component ref={ref}>
|
|
<View />
|
|
</props.Component>
|
|
<Text>
|
|
{props.name + ': '}
|
|
{status == null
|
|
? 'Missing Ref!'
|
|
: status === true
|
|
? 'Native Methods Exist'
|
|
: 'Native Methods Missing!'}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function TouchableNativeMethods() {
|
|
return (
|
|
<View>
|
|
<TouchableNativeMethodChecker
|
|
Component={TouchableHighlight}
|
|
name="TouchableHighlight"
|
|
/>
|
|
<TouchableNativeMethodChecker
|
|
Component={TouchableOpacity}
|
|
name="TouchableOpacity"
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
class TouchableDisabled extends React.Component<{...}> {
|
|
render() {
|
|
return (
|
|
<View>
|
|
<TouchableOpacity disabled={true} style={[styles.row, styles.block]}>
|
|
<Text style={styles.disabledButton}>Disabled TouchableOpacity</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity disabled={false} style={[styles.row, styles.block]}>
|
|
<Text style={styles.button}>Enabled TouchableOpacity</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableHighlight
|
|
activeOpacity={1}
|
|
disabled={true}
|
|
underlayColor="rgb(210, 230, 255)"
|
|
style={[styles.row, styles.block]}
|
|
onPress={() => console.log('custom THW text - highlight')}>
|
|
<Text style={styles.disabledButton}>Disabled TouchableHighlight</Text>
|
|
</TouchableHighlight>
|
|
|
|
<TouchableHighlight
|
|
activeOpacity={1}
|
|
underlayColor="rgb(210, 230, 255)"
|
|
style={[styles.row, styles.block]}
|
|
onPress={() => console.log('custom THW text - highlight')}>
|
|
<Text style={styles.button}>Enabled TouchableHighlight</Text>
|
|
</TouchableHighlight>
|
|
|
|
<TouchableWithoutFeedback
|
|
onPress={() => console.log('TWOF has been clicked')}
|
|
disabled={true}>
|
|
<View style={styles.wrapperCustom}>
|
|
<Text
|
|
style={[
|
|
styles.button,
|
|
styles.nativeFeedbackButton,
|
|
styles.disabledButton,
|
|
]}>
|
|
Disabled TouchableWithoutFeedback
|
|
</Text>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
|
|
<TouchableWithoutFeedback
|
|
onPress={() => console.log('TWOF has been clicked')}
|
|
disabled={false}>
|
|
<View style={styles.wrapperCustom}>
|
|
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
|
Enabled TouchableWithoutFeedback
|
|
</Text>
|
|
</View>
|
|
</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
|
|
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(
|
|
150,
|
|
)}>
|
|
<View>
|
|
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
|
radius 150
|
|
</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',
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
row: {
|
|
justifyContent: 'center',
|
|
flexDirection: 'row',
|
|
},
|
|
centered: {
|
|
justifyContent: 'center',
|
|
},
|
|
image: {
|
|
width: 50,
|
|
height: 50,
|
|
},
|
|
text: {
|
|
fontSize: 16,
|
|
},
|
|
block: {
|
|
padding: 10,
|
|
},
|
|
button: {
|
|
color: '#007AFF',
|
|
},
|
|
disabledButton: {
|
|
color: '#007AFF',
|
|
opacity: 0.5,
|
|
},
|
|
nativeFeedbackButton: {
|
|
textAlign: 'center',
|
|
margin: 10,
|
|
},
|
|
hitSlopButton: {
|
|
color: 'white',
|
|
},
|
|
wrapper: {
|
|
borderRadius: 8,
|
|
},
|
|
wrapperCustom: {
|
|
borderRadius: 8,
|
|
padding: 6,
|
|
},
|
|
hitSlopWrapper: {
|
|
backgroundColor: 'red',
|
|
marginVertical: 30,
|
|
},
|
|
logBox: {
|
|
padding: 20,
|
|
margin: 10,
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
borderColor: '#f0f0f0',
|
|
backgroundColor: '#f9f9f9',
|
|
},
|
|
eventLogBox: {
|
|
padding: 10,
|
|
margin: 10,
|
|
height: 120,
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
borderColor: '#f0f0f0',
|
|
backgroundColor: '#f9f9f9',
|
|
},
|
|
forceTouchBox: {
|
|
padding: 10,
|
|
margin: 10,
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
borderColor: '#f0f0f0',
|
|
backgroundColor: '#f9f9f9',
|
|
alignItems: 'center',
|
|
},
|
|
textBlock: {
|
|
fontWeight: '500',
|
|
color: 'blue',
|
|
},
|
|
});
|
|
|
|
exports.displayName = (undefined: ?string);
|
|
exports.description = 'Touchable and onPress examples.';
|
|
exports.title = '<Touchable*> and onPress';
|
|
exports.examples = [
|
|
{
|
|
title: '<TouchableHighlight>',
|
|
description: ('TouchableHighlight works by adding an extra view with a ' +
|
|
'black background under the single child view. This works best when the ' +
|
|
'child view is fully opaque, although it can be made to work as a simple ' +
|
|
'background color change as well with the activeOpacity and ' +
|
|
'underlayColor props.': string),
|
|
render: function(): React.Node {
|
|
return <TouchableHighlightBox />;
|
|
},
|
|
},
|
|
{
|
|
title: '<TouchableWithoutFeedback>',
|
|
render: function(): React.Node {
|
|
return <TouchableWithoutFeedbackBox />;
|
|
},
|
|
},
|
|
{
|
|
title: 'TouchableNativeFeedback with Animated child',
|
|
description: ('TouchableNativeFeedback can have an AnimatedComponent as a' +
|
|
'direct child.': string),
|
|
platform: 'android',
|
|
render: function(): React.Node {
|
|
const mScale = new Animated.Value(1);
|
|
Animated.timing(mScale, {
|
|
toValue: 0.3,
|
|
duration: 1000,
|
|
useNativeDriver: false,
|
|
}).start();
|
|
const style = {
|
|
backgroundColor: 'rgb(180, 64, 119)',
|
|
width: 200,
|
|
height: 100,
|
|
transform: [{scale: mScale}],
|
|
};
|
|
return (
|
|
<View>
|
|
<View style={styles.row}>
|
|
<TouchableNativeFeedback>
|
|
<Animated.View style={style} />
|
|
</TouchableNativeFeedback>
|
|
</View>
|
|
</View>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '<Text onPress={fn}> with highlight',
|
|
render: function(): React.Element<any> {
|
|
return <TextOnPressBox />;
|
|
},
|
|
},
|
|
{
|
|
title: 'Touchable feedback events',
|
|
description: ('<Touchable*> components accept onPress, onPressIn, ' +
|
|
'onPressOut, and onLongPress as props.': string),
|
|
render: function(): React.Element<any> {
|
|
return <TouchableFeedbackEvents />;
|
|
},
|
|
},
|
|
{
|
|
title: 'Touchable delay for events',
|
|
description: ('<Touchable*> components also accept delayPressIn, ' +
|
|
'delayPressOut, and delayLongPress as props. These props impact the ' +
|
|
'timing of feedback events.': string),
|
|
render: function(): React.Element<any> {
|
|
return <TouchableDelayEvents />;
|
|
},
|
|
},
|
|
{
|
|
title: '3D Touch / Force Touch',
|
|
description:
|
|
'iPhone 8 and 8 plus support 3D touch, which adds a force property to touches',
|
|
render: function(): React.Element<any> {
|
|
return <ForceTouchExample />;
|
|
},
|
|
platform: 'ios',
|
|
},
|
|
{
|
|
title: 'Touchable Hit Slop',
|
|
description: ('<Touchable*> components accept hitSlop prop which extends the touch area ' +
|
|
'without changing the view bounds.': string),
|
|
render: function(): React.Element<any> {
|
|
return <TouchableHitSlop />;
|
|
},
|
|
},
|
|
{
|
|
title: 'Touchable Native Methods',
|
|
description: ('Some <Touchable*> components expose native methods like `measure`.': string),
|
|
render: function(): React.Element<any> {
|
|
return <TouchableNativeMethods />;
|
|
},
|
|
},
|
|
{
|
|
title: 'Custom Ripple Radius (Android-only)',
|
|
description: ('Ripple radius on TouchableNativeFeedback can be controlled': string),
|
|
render: function(): React.Element<any> {
|
|
return <CustomRippleRadius />;
|
|
},
|
|
},
|
|
{
|
|
title: 'Disabled Touchable*',
|
|
description: ('<Touchable*> components accept disabled prop which prevents ' +
|
|
'any interaction with component': string),
|
|
render: function(): React.Element<any> {
|
|
return <TouchableDisabled />;
|
|
},
|
|
},
|
|
];
|