I need some buttons that can be pressed at the same time, but currently if you press one, it 'claims' responsiveness and the others can't be pressed anymore. How do I do this?
Got it. You have to use ReactNativeEventEmitter to directly listen to touch events and bypass the Gesture Responder stuff entirely. Below is a decorator class that calls onTouchStart, onTouchEnd and onTouchMove in the wrapped class whenever those touch events are received.
'use strict';
import React, {Component} from 'react-native';
import ReactNativeEventEmitter from 'ReactNativeEventEmitter';
import NodeHandle from 'NodeHandle';
export const multitouchable = BaseComponent => {
return class extends Component {
constructor(props, context) {
super(props, context);
this.comp = null;
this.compId = null;
}
componentDidMount() {
if(this.comp && this.compId){
this.comp.onTouchStart && ReactNativeEventEmitter.putListener(this.compId, 'onTouchStart', e => this.comp.onTouchStart(e));
this.comp.onTouchEnd && ReactNativeEventEmitter.putListener(this.compId, 'onTouchEnd', e => this.comp.onTouchEnd(e));
this.comp.onTouchMove && ReactNativeEventEmitter.putListener(this.compId, 'onTouchMove', e => this.comp.onTouchMove(e));
}
}
componentWillUnmount() {
if(this.comp && this.compId){
this.comp.onTouchStart && ReactNativeEventEmitter.deleteListener(this.compId, 'onTouchStart');
this.comp.onTouchEnd && ReactNativeEventEmitter.deleteListener(this.compId, 'onTouchEnd');
this.comp.onTouchMove && ReactNativeEventEmitter.deleteListener(this.compId, 'onTouchMove');
}
}
render() {
return (
<BaseComponent {...this.props} {...this.state}
ref={c => {
this.comp = c;
const handle = React.findNodeHandle(c);
if(handle)
this.compId = NodeHandle.getRootNodeID(handle);
}}
/>
);
}
};
}
Related
I am working on a react native app and I have an array of components that I need to be updated from different sections of my app. I am using context to be able to provide different consumers with this array, as well as the ability to the update the array.
As you can see in the code below I am passing my array, as well as a function to update the list, as the value for my provider. I then am using "header-right" in the navigation to call the function that updates the array in the provider state. I have confirmed that the array is updating and will only re-render the consumers when a change state in another component causes a re-render.
What I need is the change state of the provider to re-render all consumer components when it is changed, not when another components state change has occurred.
const DbContext = React.createContext("DEFAULT");
export default class DbProvider extends Component {
constructor(props){
super(props);
this.state = {
choreList: [],
refresh:null,
}
}
componentDidMount(){
sqlQueries.getChores().then(row => this.setChoresList(row));
}
updateChoreList = (choresAdded, offset) => {
console.log("K");
if(offset == 1){ // if adding chores
var tempList = this.state.choreList;
tempList.push(choresAdded);
this.setState({
choreList: tempList,
})
}else if(offset == -1){ // if removing a chore
for(var i = 0; i < this.state.choreList.length; i++){
for(var j = 0; j < choresAdded.length; j++){
if(this.state.choreList[i].state.name == choresAdded[i].state.name){ // if chore being removed is i in state choreList
var tempList = this.state.choreList; // remove and update state choreList
tempList.splice(i, 1);
this.setState({
choreList: tempList,
});
break;
}
}
}
}
}
setChoresList(row){
var tempList = [];
for(var i = 0; i < row._array.length; i++){
tempList.push(<Chore choreName={row._array[i].name} dayId={row._array[i].id} nav={this.props} inDB={true} key={row._array[i].name}/>);
}
this.setState({
choreList: tempList,
})
}
render () {
return (
<DbContext.Provider value={{choreList: this.state.choreList, update: this.updateChoreList}}>
{this.props.children}
</DbContext.Provider>
)
}
}
export { DbContext, DbProvider }
I am then in another component using this list to display something
render(){
return (
<DbProvider>
<DbContext.Consumer>
{globalChores => globalChores.choreList == 0
? <NoChores nav={this.props.nav} dayState={this} />
: (
<View style={styles.ChoreDayContainer}>
<View style={styles.ChoreListContainer}>
{globalChores.choreList}
</View>
<AddChoresBtn nav={this.props.nav} dayState={this}/>
</View>)}
</DbContext.Consumer>
</DbProvider>
);
}
and finally I am updating the provided array like so
componentDidMount(){
this.props.navigation.setOptions({
headerRight: () => (
<DbProvider>
<DbContext.Consumer>{(db) =>(
<Button
onPress={ () => this.dataToDB(db)}
title="Save"
color="#fff"
/> )}
</DbContext.Consumer></DbProvider>
),
})
}
dataToDB(db){
db.update(this.state.choreShowingList, 1);
}
}
I'm building an app with react-navigation-4.2.1. The app has multiple stack navigators. So there are a lots of navigation.push('Routename') calls.
Trouble is when the control surface (i.e. TouchableOpacity) is tapped rapidly multiple times (first one, and the rest during screen transition) I end up pushing multiple screens into the stack. Is there a way to restrict the surface to the first tap/call of push()?
The component below is what i use to make things touchable. it handle multiple touches in small period of time.
Use component below instead of TouchableOpacity. wrap any thing you want with this component and it will be touchable.
<SafeTouch
onPress={...}
>
<Text> hey! im a touchable text now</Text>
</SafeTouch>
The component below is written used TypeScirpt.
every touch within 300ms after first touch will be ignored(thats where help you with your problem).
import * as React from 'react'
import { TouchableOpacity } from 'react-native'
interface ISafeTouchProps {
onPress: () => void
onLongPress?: () => void
onPressIn?: () => void
onPressOut?: () => void,
activeOpacity?: number,
disabled?: boolean,
style: any
}
export class SafeTouch extends React.PureComponent<ISafeTouchProps> {
public static defaultProps: ISafeTouchProps = {
onPress: () => { },
onLongPress: () => { },
onPressIn: () => { },
onPressOut: () => { },
disabled: false,
style: null
}
private isTouchValid: boolean = true
private touchTimeout: any = null
public constructor(props: ISafeTouchProps) {
super(props)
{// Binding methods
this.onPressEvent = this.onPressEvent.bind(this)
}
}
public render(): JSX.Element {
return (
<TouchableOpacity
onPress={this.onPressEvent}
onLongPress={this.props.onLongPress}
onPressIn={this.props.onPressIn}
onPressOut={this.props.onPressOut}
activeOpacity={this.props.activeOpacity}
disabled={this.props.disabled}
style={[{minWidth: 24, minHeight: 24}, this.props.style]}
>
{
this.props.children
}
</TouchableOpacity>
)
}
public componentWillUnmount() {
this.clearTimeoutIfExists()
}
private onPressEvent(): void {
requestAnimationFrame(() => {
if (this.isTouchValid === false) {
return
}
this.isTouchValid = false
this.clearTimeoutIfExists()
this.touchTimeout = setTimeout(() => {
this.isTouchValid = true
}, 300)
if (typeof this.props.onPress === 'function') {
this.props.onPress()
}
})
}
private clearTimeoutIfExists(): void {
if (this.touchTimeout != null) {
clearTimeout(this.touchTimeout)
this.touchTimeout = null
}
}
}
This is the proper behavior for Push and it is not a bug if you want
to avoid the duplicate screen on double tab you can just use navigation.navigate.
To avoid pushing the screen more than once when clicking in the same button in a short span of time, I created a generic hook to avoid running a function more than once (accepting an interval to allow run again):
export const useCallOnce = <T extends unknown[], K>(
fn: (...args: T) => K,
allowAfter?: number,
) => {
const ref = React.useRef<number | undefined>();
const resultFn = (...args: T) => {
const now = new Date().getTime();
if (!ref.current || (allowAfter && ref.current + allowAfter < now)) {
ref.current = now;
return fn(...args);
}
};
return resultFn;
};
Then, you can just call it as in the following example:
const navigation = useNavigation<NativeStackNavigationProp<{ ExampleScreen: undefined }>>();
const push = useCallOnce(() => navigation.push('ExampleScreen'), 500);
// just call on the button click event as: onSomeEvent={() => push()}
You can create a generic button component that accept the push parameters with the hook above, similar to the example, and use this button whenever you want a button to navigate between pages.
I need to perform different action on single and double tap on a view. On double tap I need to like the image just like Instagram double tap experience. On single tap I need to open a modal.
For double tap I have used TapGestureHandler which works perfect
<TapGestureHandler
ref={this.doubleTapRef}
maxDelayMs={200}
onHandlerStateChange={this.onProductImageDoubleTap}
numberOfTaps={2}
>
<SomeChildComponent ...
But when I add any Touchable to detect single tap in the
<TouchableWithoutFeedback onPress={this.imageTapped}>
on double tapping the this.imageTapped function is called twice along with this.onProductImageDoubleTap. Is there any way to cancel tap on touchable when two taps are done is quick succession
The best solution is not using state, since setting state is asynchronous.Works like a charm for me on android !
let lastPress = 0;
const functionalComp = () => {
const onDoublePress = () => {
const time = new Date().getTime();
const delta = time - lastPress;
const DOUBLE_PRESS_DELAY = 400;
if (delta < DOUBLE_PRESS_DELAY) {
// Success double press
console.log('double press');
}
lastPress = time;
};
return <View
onStartShouldSetResponder =
{(evt) => onDoublePress()}>
</View>
}
2022 update
This is a performant native solution without any JS thread blocking calculation!
Many more tips here
const tap = Gesture.Tap()
.numberOfTaps(2)
.onStart(() => {
console.log('Yay, double tap!');
});
return (
<GestureDetector gesture={tap}>
{children}
</GestureDetector>
);
The best solution is use react-native-gesture-handler
https://github.com/software-mansion/react-native-gesture-handler
Here is my solution -
import {State, TapGestureHandler} from 'react-native-gesture-handler';
export const DoubleTap = ({children}: any) => {
const doubleTapRef = useRef(null);
const onSingleTapEvent = (event: any) => {
if (event.nativeEvent.state === State.ACTIVE) {
console.log("single tap 1");
}
};
const onDoubleTapEvent = (event: any) => {
if (event.nativeEvent.state === State.ACTIVE) {
console.log("double tap 1");
}
};
return (
<TapGestureHandler
onHandlerStateChange={onSingleTapEvent}
waitFor={doubleTapRef}>
<TapGestureHandler
ref={doubleTapRef}
onHandlerStateChange={onDoubleTapEvent}
numberOfTaps={2}>
{children}
</TapGestureHandler>
</TapGestureHandler>
);
};
Now we will wrap the component where we need to detect the double and single tap : -
<DoubleTap>
<View>
...some view and text
</View>
</DoubleTap>
The package react-native-double-tap seems to be what you are looking for.
since you are asking on handling one tap and double tap, here's a simple code i think should covered your issue
Untested
first defined clickCount:0 in state:
state={clickCount:0, //another state}
then create a function with setTimeout to handling if user tapping once or two times:
handlingTap(){
this.state.clickCount==1 ?
//user tap twice so run this.onProductImageDoubleTap()
this.onProductImageDoubleTap :
//user tap once so run this.imageTapped with setTimeout and setState
//i set 1 sec for double tap, so if user tap twice more than 1 sec, it's count as one tap
this.setState({clickCount:1}, ()=>{
setTimeout(()=>{
this.setState({clickCount:0})
this.imageTapped()
}, 1000)
})
}
<TouchableWithoutFeedback onPress={this.handlingTap()}/>
just used TouchableWithoutFeedback instead of TapGestureHandler
With hooks:
const [lastPressed, setLastPressed] = useState(0);
const handlePress = useCallback(() => {
const time = new Date().getTime();
const delta = time - lastPressed;
setLastPressed(time);
if (lastPressed) {
if (delta < DOUBLE_PRESS_DELAY) {
console.log('double press');
} else {
console.log('single press');
}
}
}, [lastPressed]);
I have modified flix's answer into this. By this way, you can catch one and double click separately. I also changed debounce into 300ms which is fairly well for one and double click.
state={clickCount:0, //another state}
Binding context into the handlingTap method
constructor(props){
super(props)
this.handlingTap = this.handlingTap.bind(this)
}
With this function you can catch them separately
handlingTap() {
this.state.clickCount === 1
? this.doubleClick() // This catches double click
: this.setState(state => ({ clickCount: state.clickCount + 1 }), () => {
setTimeout(() => {
if (this.state.clickCount !== 2) {
this.oneClick() // this catches one click
}
this.setState({ clickCount: 0 })
}, 300)
})
}
In the button you can use this way
<TouchableWithoutFeedback onPress={this.handlingTap}></TouchableWithoutFeedback>
hai i am trying to move the view up when keyboard as shown using react-native,I followed the #sherlock's comment in (How to auto-slide the window out from behind keyboard when TextInput has focus? i got an error like this
I don't know how to resolve this error, can any one help me how to resolve this, any help much appreciated.
There's a great discussion about this in the react-native github issues
https://github.com/facebook/react-native/issues/3195#issuecomment-147427391
I'd start there, but here are a couple more links you may find useful, one of which is mentioned already in the article you referenced...
[React Tips] Responding to the keyboard with React Native
Andr3wHur5t/react-native-keyboard-spacer
In my library "react-native-form-generator" (https://github.com/MichaelCereda/react-native-form-generator) i did the following.
I created a Keyboard Aware scroll view (partially modified from https://github.com/facebook/react-native/issues/3195#issuecomment-146518331)
the following it's just an excerpt
export class KeyboardAwareScrollView extends React.Component {
constructor (props) {
super(props)
this.state = {
keyboardSpace: 0,
}
this.updateKeyboardSpace = this.updateKeyboardSpace.bind(this)
this.resetKeyboardSpace = this.resetKeyboardSpace.bind(this)
}
updateKeyboardSpace (frames) {
let coordinatesHeight = frames.endCoordinates.height;
const keyboardSpace = (this.props.viewIsInsideTabBar) ? coordinatesHeight - 49 : coordinatesHeight
this.setState({
keyboardSpace: keyboardSpace,
})
}
resetKeyboardSpace () {
this.setState({
keyboardSpace: 0,
})
}
componentDidMount () {
// Keyboard events
DeviceEventEmitter.addListener('keyboardWillShow', this.updateKeyboardSpace)
DeviceEventEmitter.addListener('keyboardWillHide', this.resetKeyboardSpace)
}
componentWillUnmount () {
DeviceEventEmitter.removeAllListeners('keyboardWillShow')
DeviceEventEmitter.removeAllListeners('keyboardWillHide')
}
scrollToFocusedInput (event, reactNode, extraHeight = 69) {
const scrollView = this.refs.keyboardScrollView.getScrollResponder();
setTimeout(() => {
scrollView.scrollResponderScrollNativeHandleToKeyboard(
reactNode, extraHeight, true
)
}, 220)
}
render () {
return (
<ScrollView
ref='keyboardScrollView'
keyboardDismissMode='interactive'
contentInset={{bottom: this.state.keyboardSpace}}
showsVerticalScrollIndicator={true}
style={this.props.style}>
{this.props.children}
</ScrollView>
)
}
Then i use it like any other scrollview
import { KeyboardAwareScrollView } from 'react-native-form-generator'
...
handleFormFocus(event, reactNode){
this.refs.scroll.scrollToFocusedInput(event, reactNode)
}
...
<KeyboardAwareScrollView ref='scroll'>
<Form ref='registrationForm'
onFocus={this.handleFormFocus.bind(this)}
onChange={this.handleFormChange.bind(this)}
label="Personal Information">
........
</Form>
</KeyboardAwareScrollView>
on change my component (Form) will call scrollToFocusedInput in KeyboardAwareScrollView (using the ref).
i suggest to check the code of my library (see the link on top), or simply use it (everything it's already tested and working).
If you have further questions just comment
How Can I Set back to last activity in react-native for android?
My thinking is:
1 set _navigate in index.android.js
// 设置回退的堆栈
var _navigator;
BackAndroid.addEventListener('hardwareBackPress', function() {
if (_navigator && _navigator.getCurrentRoutes().length > 1) {
_navigator.pop();
return true;
}
return false;
});
2 pass navigator in RouteMapping:
RouteMapper: function(route, navigationOperations, onComponentRef) {
_navigator = navigationOperations;
if (route.name === 'detail') {
// 问题详情页
return (
<DetailScreen
navigator={navigationOperations}
question={route.question} />
);
} else if (route.name == 'front') {
// 首页
return(
<FrontScreen
navigator={navigationOperations}
/>
);
}
},
3 set push in list view
gotoDetail: function(question: Object) {
this.props.navigator.push({
id: question.question_id,
name: 'detail',
question: question
})
But It not work. When I click back button in Android, it jump out the app?
How Can I do that?
Or Anyone can give some example?
You need to add the following code to <your project>/android/app/src/main/java/com/<your project>/MainActivity.java:
#Override
public void onBackPressed() {
if (mReactInstanceManager != null) {
mReactInstanceManager.onBackPressed();
} else {
super.onBackPressed();
}
}
Projects created with React Native 0.12 will behave correctly.
For more infos see this issue on Github https://github.com/facebook/react-native/issues/3223.