How to get the position of the currently focussed input? - react-native

I need to know the position of the currently focussed input to make some calculations. At the end I want to call .measure() on it.
What I got:
import { TextInput } from 'react-native';
const { State: TextInputState } = TextInput;
var currentlyFocussedField = TextInputState.currentlyFocusedField();
console.log('currentlyFocussedField', currentlyFocussedField);
But this only returns the id of the text field (like 394, and the next field is 395). How can I access the real element/object or how do I get its position on the screen?

I don't know how you can access the React component from the numeric reactTag in javascript, but you can use the same UIManager.measure(reactTag, callback) function that RN uses internally to make .measure() work.
It's not documented, but you can find the source here, for iOS, and here for Android.
You can use this to get the all the size values of the native view. For example, drawing from your example above:
import { UIManager, TextInput } from 'react-native';
const { State: TextInputState } = TextInput;
const currentlyFocusedField = TextInputState.currentlyFocusedField();
UIManager.measure(currentlyFocusedField, (originX, originY, width, height, pageX, pageY) => {
console.log(originX, originY, width, height, pageX, pageY);
});

This is how I'm getting the position of a focused input to scroll to it:
import { findNodeHandle, ScrollView, TextInput, UIManager } from 'react-native'
class SomeForm extends Component {
scrollTo (element) {
const handle = findNodeHandle(this[element])
if (handle) {
UIManager.measure(handle, (x, y) => {
this._scrollView.scrollTo({
animated: true,
y
})
})
}
}
render () {
return (
<ScrollView
ref={(element) => {
this._scrollView = element
}}
>
<TextInput
onFocus={() => this.scrollTo('_inputName')}
ref={(element) => {
this._inputName = element
}}
/>
</ScrollView>
)
}
}

Instead of:
var currentlyFocussedField = TextInputState.currentlyFocusedField();
use:
var currentlyFocussedField = TextInputState.currentlyFocusedInput();
Then you can:
currentlyFocussedField.measure(...)

Related

Get the last scroll from a FlatList and redirect

I'm creating an Onboarding screen. I don't want to add buttons to this onboarding screen, the only way to go to the next page is by swiping. Although I need to have a way to get when the user is at the last screen, and when he swipes right, I will be redirecting him to the login screen.
This is the last screen on the app:
When the user swipes right from this screen, I want to make a redirect, like a function or a onPress.
This is the onboarding code:
import React, { useState, useRef } from 'react';
import {
Container,
FlatListContainer
} from './styles';
import {
FlatList,
Animated,
TouchableOpacity
} from 'react-native'
import OnboardingData from '../../utils/onboarding';
import { OnboardingItem } from '../../components/OnboardingItem';
import { Paginator } from '../../components/Paginator';
export function Onboarding(){
const [currentIndex, setCurrentIndex] = useState(0);
const scrollX = useRef(new Animated.Value(0)).current;
const scrollY = useRef(new Animated.Value(0)).current;
const onboardingDataRef = useRef(null);
const viewableItemsChanged = useRef(({ viewableItems }: any) => {
setCurrentIndex(viewableItems[0].index);
}).current;
const viewConfig = useRef({ viewAreaCoveragePercentThreshold: 50 }).current;
return (
<Container>
<FlatListContainer>
<FlatList
data={OnboardingData}
renderItem={({ item }) => <OnboardingItem image={item.image} title={item.title} description={item.description}/>}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled={true}
bounces={false}
keyExtractor={(item) => String(item.id)}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } }}], {
useNativeDriver: false
})}
scrollEventThrottle={32}
onViewableItemsChanged={viewableItemsChanged}
viewabilityConfig={viewConfig}
ref={onboardingDataRef}
/>
</FlatListContainer>
<Paginator data={OnboardingData} scrollX={scrollX} scrollY={scrollY} currentIndex={currentIndex}/>
</Container>
);
}
I thought about it, and the old method wouldnt work, as there isnt a viewableItem beyond the final page.
You could use the FlatList onScroll function to listen for swipes. When at the final page and a scroll event occurs, verify that it wasnt an attempt to go to the previous page and then navigate. e.g.
// store previous scroll values
const prevScroll = useRef({});
// scrollX, scrollY already contains the data
useEffect(()=>{
let isLastIndex = currentIndex == onBoardingData.length - 1;
let isScrollingToNextPage = scrollX > prevScroll.current.x
if( isLastIndex && isScrollingToNextPage ){
props.navigation.navigate("Login");
return
}
// store offsets
prevScroll.current = { x:scrollX, y:scrollY}
},[scrollX, scrollY])
If you would like for the old method to work you could just add an additional page (could be an empty view or a view with a small message)
--------OLD--------
Im assuming that OnBordingData is an array since you're passing it to the Flatlist data prop. If so, then couldnt you add a conditional to your viewableItemsChanged so that it goes to the login screen when at index > OnBoardingData.length - 1? e.g
const viewableItemsChanged = useRef(({ viewableItems }: any) => {
const index = viewableItems[0].index;
if(index >= OnBoardinData.length -1){
// be sure to add props as a parameter to OnBoarding function
props.navigation.navigate("Login");
return
}
setCurrentIndex(index);
}).current;
--------OLD--------

How to update theme when device orientation changes

I am trying to implement orientation changing with hooks. I called the orientation hook from app.tsx and I want to update everything(theme,style in component) that uses widthPercentageToDP() function. How can I achieve this. I can't figured out.
useOrientation.tsx
export let { width, height } = Dimensions.get("window");
const heightPercentageToDP = (heightPercent: string | number): number => {
// Parse string percentage input and convert it to number.
const elemHeight =
typeof heightPercent === "number"
? heightPercent
: parseFloat(heightPercent);
// Use PixelRatio.roundToNearestPixel method in order to round the layout
// size (dp) to the nearest one that correspons to an integer number of pixels.
return PixelRatio.roundToNearestPixel((height * elemHeight) / 100);
};
export const useScreenDimensions = () => {
const [screenData, setScreenData] = useState({});
useEffect(() => {
setScreenData({orientation:currentOrientation()});
Dimensions.addEventListener("change", (newDimensions) => {
width = newDimensions.screen.width;
height = newDimensions.screen.height;
setScreenData({orientation:currentOrientation()}); // can be used with this height and width
//console.log(newDimensions.window);
});
return () => Dimensions.removeEventListener("change", () => {});
});
return {
width,height,
screenData
};
};
Theme file
const theme = {
spacing: {
m:widthPercentageToDP("2%") // it must be updated when orientation changes.
},
borderRadii: {
s:widthPercentageToDP("5%") // it must be updated when orientation changes.
},
textVariants: {
body:{
fontSize:widthPercentageToDP("%3"),
}
},
};
App.tsx
const {screenData} = useScreenDimensions();
console.log(screenData)
return (
<ThemeProvider>
<LoadAssets {...{ fonts, assets }}>
<Example/>
</LoadAssets>
</ThemeProvider>
);
}
Example.tsx
export const Example = ({}) => {
return (
<Box>
<Text variant="body">hey</Text>
{/* // it must be updated when orientation changes. */}
<View style={{width:widthPercentageToDP("40%")}}/>
</Box>
);
}
Box and theme come from theme.tsx file. Text component accepts variant prop that defined in theme.tsx
Using react-native-orientation you can do what you want, then the device orientation changes.
Example:
import Orientation from 'react-native-orientation';
export default class AppScreen extends Component {
componentWillMount() {
const initial = Orientation.getInitialOrientation();
if (initial === 'PORTRAIT') {
// do something
} else {
// do something else
}
},
componentDidMount() {
// this will listen for changes
Orientation.addOrientationListener(this._orientationDidChange);
},
_orientationDidChange = (orientation) => {
if (orientation === 'LANDSCAPE') {
// do something with landscape layout
} else {
// do something with portrait layout
}
},
componentWillUnmount() {
// Remember to remove listener to prevent memory leaks
Orientation.removeOrientationListener(this._orientationDidChange);
}

How to dynamically change React Native transform with state?

I'm building a custom view that will rotate its contents based on device orientation. This app has orientation locked to portrait and I just want to rotate a single view. It fetches the current device orientation, updates the state, then renders the new component with the updated style={{transform: [{rotate: 'xxxdeg'}]}}.
I'm using react-native-orientation-locker to detect orientation changes.
The view renders correctly rotated on the first render. For example, if the screen loads while the device is rotated, it will render the view rotated. But upon changing the orientation of the device or simulator, the view does not rotate. It stays locked at the rotate value it was initialized at.
It seems like updates to the transform rotate value do not change the rotation. I've verified that new rotate values are present during the render. I've verified that orientation changes are correctly updating the state. But the view is never rotated in the UI when orientation changes. It is as if React Native isn't picking up on changes to the rotate value during a render.
I would expect that updates to the rotate value would rotate the View accordingly but that does not seem to be the case. Is there another way to accomplish this or do I have a bug in this code?
Edit: Is it required for rotate to be an Animated value?
import React, {useState, useEffect} from 'react';
import {View} from 'react-native';
import Orientation from 'react-native-orientation-locker';
const RotateView = props => {
const getRotation = newOrientation => {
switch (newOrientation) {
case 'LANDSCAPE-LEFT':
return '90deg';
case 'LANDSCAPE-RIGHT':
return '-90deg';
default:
return '0deg';
}
};
const [orientation, setOrientation] = useState(
// set orientation to the initial device orientation
Orientation.getInitialOrientation(),
);
const [rotate, setRotate] = useState(
// set rotation to the initial rotation value (xxdeg)
getRotation(Orientation.getInitialOrientation()),
);
useEffect(() => {
// Set up listeners for device orientation changes
Orientation.addDeviceOrientationListener(setOrientation);
return () => Orientation.removeDeviceOrientationListener(setOrientation);
}, []);
useEffect(() => {
// when orientation changes, update the rotation
setRotate(getRotation(orientation));
}, [orientation]);
// render the view with the current rotation value
return (
<View style={{transform: [{rotate}]}}>
{props.children}
</View>
);
};
export default RotateView;
I had this same problem, and solved it by using an Animated.View from react-native-reanimated. (Animated.View from the standard react-native package might also work, but I haven't checked). I didn't need to use an Animated value, I still just used the actual value from the state, and it worked.
If you use Animated.Value + Animated.View directly from react native you'll be fine.
Had the same issue and solved it using an Animated.Value class field (in your case I guess you'd use a useState for this one since functional + a useEffect to set the value of the Animated.Value upon changes in props.rotation), and then pass that into the Animated.View as the transform = [{ rotate: animatedRotationValue }]
Here's the class component form of this as a snippet:
interface Props {
rotation: number;
}
class SomethingThatNeedsRotation extends React.PureComponent<Props> {
rotation = new Animated.Value(0);
rotationValue = this.rotation.interpolate({
inputRange: [0, 2 * Math.PI],
outputRange: ['0deg', '360deg'],
});
render() {
this.rotation.setValue(this.props.rotation);
const transform = [{ rotate: this.rotationValue }];
return (
<Animated.View style={{ transform }} />
);
}
}
Note that in my example I also have the interpolation there since my input is in radians and I wanted it to be in degrees.
Here is my completed component that handles rotation. It will rotate its children based on device orientation while the app is locked to portrait. I'm sure this could be cleaned up some but it works for my purposes.
import React, {useState, useEffect, useRef} from 'react';
import {Animated, Easing, View, StyleSheet} from 'react-native';
import {Orientation} from '../utility/constants';
import OrientationManager from '../utility/orientation';
const OrientedView = (props) => {
const getRotation = useRef((newOrientation) => {
switch (newOrientation) {
case Orientation.LANDSCAPE_LEFT:
return 90;
case Orientation.LANDSCAPE_RIGHT:
return -90;
default:
return 0;
}
});
const {duration = 100, style} = props;
const initialized = useRef(false);
const [orientation, setOrientation] = useState();
const [rotate, setRotate] = useState();
const [containerStyle, setContainerStyle] = useState(styles.containerStyle);
// Animation kept as a ref
const rotationAnim = useRef();
// listen for orientation changes and update state
useEffect(() => {
OrientationManager.getDeviceOrientation((initialOrientation) => {
const initialRotation = getRotation.current(initialOrientation);
// default the rotation based on initial orientation
setRotate(initialRotation);
rotationAnim.current = new Animated.Value(initialRotation);
setContainerStyle([
styles.containerStyle,
{
transform: [{rotate: `${initialRotation}deg`}],
},
]);
initialized.current = true;
// set orientation and trigger the first render
setOrientation(initialOrientation);
});
OrientationManager.addDeviceOrientationListener(setOrientation);
return () =>
OrientationManager.removeDeviceOrientationListener(setOrientation);
}, []);
useEffect(() => {
if (initialized.current === true) {
const rotation = getRotation.current(orientation);
setRotate(
rotationAnim.current.interpolate({
inputRange: [-90, 0, 90],
outputRange: ['-90deg', '0deg', '90deg'],
}),
);
Animated.timing(rotationAnim.current, {
toValue: rotation,
duration: duration,
easing: Easing.ease,
useNativeDriver: true,
}).start();
}
}, [duration, orientation]);
// FIXME: This is causing unnessary animation outside of the oriented view. Disabling removes the scale animation.
// useEffect(() => {
// applyLayoutAnimation.current();
// }, [orientation]);
useEffect(() => {
if (initialized.current === true) {
setContainerStyle([
styles.containerStyle,
{
transform: [{rotate}],
},
]);
}
}, [rotate]);
if (initialized.current === false) {
return <View style={[containerStyle, style]} />;
}
return (
<Animated.View style={[containerStyle, style]}>
{props.children}
</Animated.View>
);
};
const styles = StyleSheet.create({
containerStyle: {flex: 0, justifyContent: 'center', alignItems: 'center'},
});
export default OrientedView;
This is a bug, as the rotation is supposed to change when the value of rotate updates. A workaround is to set the View's key attribute to the rotate value as well.
For example:
return (
<View
key={rotate} // <~~~ fix!
style={{transform: [{rotate}]}}
>
{props.children}
</View>
)
I found this solution here.

How to detect the swipe back action in react-native(react-navigation)? [duplicate]

How can I detect a left swipe on the entire screen in React Native?
Would it be necessary to use PanResponder or can it be done a little more easy?
I've found that react-native-swipe-gestures isn't stable (swipes works randomly on android) and react-native-gesture-handler is overcomplicated (too much efforts to just add to project).
Simplified solution based on Kuza Grave's answer, who's solution works perfect and very simple:
<View
onTouchStart={e=> this.touchY = e.nativeEvent.pageY}
onTouchEnd={e => {
if (this.touchY - e.nativeEvent.pageY > 20)
console.log('Swiped up')
}}
style={{height: 300, backgroundColor: '#ccc'}}
/>
I made this simple solution using scrollviews and touch position.
It has a very clean implementation with no heavy components or external modules.
You can also use this with <View> components instead of scrollviews.
So first, we will be creating a hook: useSwipe.tsx
import { Dimensions } from 'react-native';
const windowWidth = Dimensions.get('window').width;
export function useSwipe(onSwipeLeft?: any, onSwipeRight?: any, rangeOffset = 4) {
let firstTouch = 0
// set user touch start position
function onTouchStart(e: any) {
firstTouch = e.nativeEvent.pageX
}
// when touch ends check for swipe directions
function onTouchEnd(e: any){
// get touch position and screen size
const positionX = e.nativeEvent.pageX
const range = windowWidth / rangeOffset
// check if position is growing positively and has reached specified range
if(positionX - firstTouch > range){
onSwipeRight && onSwipeRight()
}
// check if position is growing negatively and has reached specified range
else if(firstTouch - positionX > range){
onSwipeLeft && onSwipeLeft()
}
}
return {onTouchStart, onTouchEnd};
}
then, in your component... in my case im going to use: exampleComponent.tsx
Import the previous useSwipe hook.
Add onTouchStart and onTouchEnd events to your scrollView.
ExampleComponent
import * as React from 'react';
import { ScrollView } from 'react-native';
import { useSwipe } from '../hooks/useSwipe'
export function ExampleComponent(props: any) {
const { onTouchStart, onTouchEnd } = useSwipe(onSwipeLeft, onSwipeRight, 6)
function onSwipeLeft(){
console.log('SWIPE_LEFT')
}
function onSwipeRight(){
console.log('SWIPE_RIGHT')
}
return (
<ScrollView onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
{props.children}
</ScrollView>
);
}
You can mess around with the offsetRange property to handle precision.
And adapt the original code to be used with normal class components instead of hooks.
There is an existing component react-native-swipe-gestures for handling swipe gestures in up, down, left and right direction, see https://github.com/glepur/react-native-swipe-gestures
You can use react-native-swipe-gesture. You don't need to install any third party module using npm. Download the file into your project and follow the given steps
If you are using a managed Expo project there is a documented gesture handler here: https://docs.expo.io/versions/latest/sdk/gesture-handler/
The source code docs can be found here: https://docs.swmansion.com/react-native-gesture-handler/docs/
For swiping I think you would need to use FlingGestureHandler:
https://docs.swmansion.com/react-native-gesture-handler/docs/handler-fling
You can just user FlingGestureHandler from react-native-gesture-handler link to the docs. Wrap your view with it. Here's you do it.
import { Directions, Gesture, GestureDetector } from 'react-native-gesture-handler'
const MyComponentWithLeftSwipe = () => {
const flingGestureLeft = Gesture
.Fling()
.direction(Directions.LEFT)
.onEnd(() => console.log("I was swiped!")
return <GestureDetector gesture={flingGestureLeft}>
<View>
...
</View>
</GestureDetector>
}
Credits to #Nikhil Gogineni!
I modified his code to a functional component without a componentWillMount.
SwipeGesture.tsx
import React, { useEffect } from 'react';
import {
View,
Animated,
PanResponder
} from 'react-native';
/* Credits to: https://github.com/nikhil-gogineni/react-native-swipe-gesture */
const SwipeGesture = (props: any) => {
const panResponder = React.useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
const x = gestureState.dx;
const y = gestureState.dy;
if (Math.abs(x) > Math.abs(y)) {
if (x >= 0) {
props.onSwipePerformed('right')
}
else {
props.onSwipePerformed('left')
}
}
else {
if (y >= 0) {
props.onSwipePerformed('down')
}
else {
props.onSwipePerformed('up')
}
}
}
})).current;
return (
<Animated.View {...panResponder.panHandlers} style={props.gestureStyle}>
<View>{props.children}</View>
</Animated.View>
)
}
export default SwipeGesture;
And the usage is the "same" ...
Thanks Nikhil!
I was using the solution provided by Kuza Grave but encountered a bug on a Samsung Galaxy phone where onTouchEnd was not being fired as expected. I ended up creating another implementation using the PanResponder.
SwipeContainer.tsx
import React, { FC, ReactNode } from "react";
import { View, Animated, PanResponder } from "react-native";
type Props = {
children: ReactNode;
onSwipeRight: () => void;
onSwipeLeft: () => void;
};
const SWIPE_THRESHOLD = 200;
export const SwipeContainer: FC<Props> = ({
children,
onSwipeLeft,
onSwipeRight,
}) => {
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: (_evt, _gestureState) => true,
onPanResponderRelease: (_evt, gestureState) => {
const { dx } = gestureState;
if (dx > SWIPE_THRESHOLD) {
onSwipeRight();
}
if (dx < -SWIPE_THRESHOLD) {
onSwipeLeft();
}
// If needed, could add up and down swipes here with `gestureState.dy`
},
});
return (
<Animated.View {...panResponder.panHandlers}>
<View>{children}</View>
</Animated.View>
);
Example.tsx:
import React, { FC } from "react";
import { ChildComponent1, ChildComponent2, SwipeContainer } from "./components";
export const Example: FC = () => {
const left = () => console.log("left");
const right = () => console.log("right");
return (
<SwipeContainer onSwipeLeft={left} onSwipeRight={right}>
<ChildComponent1 />
<ChildComponent2 />
</SwipeContainer>
);
};

ReactNative and NativeBase Radio

I've tried to change the radio value in ReactNative App with NativeBase template. I want to get or set value from the radio after click it, exactly checked or not. But couldn't find a way to get or set value to it. Even the radio button never changed on the screen after click. The codes are like as below:
import React, { Component } from 'react';
import { TouchableOpacity, Image, View } from 'react-native';
import { connect } from 'react-redux';
import { actions } from 'react-native-navigation-redux-helpers';
import {
Container,
Header,
Title,
Content,
Text,
Button,
Icon,
InputGroup,
Input,
List,
ListItem,
Radio, } from 'native-base';
import { openDrawer } from '../../actions/drawer';
import { Col, Row, Grid } from 'react-native-easy-grid';
import styles from './styles';
import dimension from './global';
import Swiper from 'react-native-swiper';
const imgBoy = require('../../../images/icon_boy.png');
const imgGirl = require('../../../images/icon_girl.png');
const {
popRoute,
} = actions;
class SessionPage extends Component {
static propTypes = {
name: React.PropTypes.string,
index: React.PropTypes.number,
list: React.PropTypes.arrayOf(React.PropTypes.string),
openDrawer: React.PropTypes.func,
popRoute: React.PropTypes.func,
navigation: React.PropTypes.shape({
key: React.PropTypes.string,
}),
}
popRoute() {
this.props.popRoute(this.props.navigation.key);
}
constructor(props) {
super(props);
// console.log(this.props.navigation);
this.state = {
sliderCount : parseInt(this.props.navigation.behavior.length / 5) + 1,
sliderArray : [],
selected : false,
}
this.getSliderArray();
console.log(this.state);
}
getSliderArray() {
for (var i = 0; i < this.state.sliderCount; i++) {
var childArray = [];
for (var j = i * 5; j < 5 * (i + 1); j++) {
if (this.props.navigation.behavior[j] != null){
var unit = this.props.navigation.behavior[j];
unit.selected = true;
childArray.push(unit);
}
}
this.state.sliderArray.push({
index : i,
behaviors : childArray
})
}
}
selectRadio(i, j){
this.state.sliderArray[i].behaviors[j].selected = true;
}
render() {
const { props: { name, index, list } } = this;
return (
<Container style={styles.container}>
<Swiper style={styles.wrapper}
height={dimension.Height - 400}
width={dimension.Width - 40}
showsButtons={false}
showsPagination={true}>
{this.state.sliderArray.map((item, i) =>
<View style={styles.slide1} key={i}>
{item.behaviors.map((subitem, j) =>
<ListItem key={i + "-" + j} style={styles.cardradio}>
<Radio selected={this.state.sliderArray[i].behaviors[j].selected} onPress={() => this.selectRadio(i, j)} />
<Text>{subitem.behaviorName}</Text>
</ListItem>
)}
</View>
)}
</Swiper>
</Content>
</Container>
);
}
}
function bindAction(dispatch) {
return {
openDrawer: () => dispatch(openDrawer()),
popRoute: key => dispatch(popRoute(key)),
};
}
const mapStateToProps = state => ({
navigation: state.cardNavigation,
name: state.user.name,
index: state.list.selectedIndex,
list: state.list.list,
});
export default connect(mapStateToProps, bindAction)(SessionPage);
selectRadio(i, j){
this.state.sliderArray[i].behaviors[j].selected = true; <== This is the problem
}
When you call this.state = something after the component has mounted, it doesn't trigger update method of component life cycle. Hence view will not be updated.
You should be using this.setState() to update your views
this.setState({
slider = something
})
For more info, refer docs
this.setState() is an async method. After you make changes in getSliderArray(), it may not be reflected in immediate console.log
this.getSliderArray();
console.log(this.state);
You can pass callback to this.setState() to perform any action only after state is changed
this.setState({
// new values
}, function() {
// Will be called only after switching to new state
})