React Native Make View "Hug" the Top of the Keyboard - javascript

Let's say I have a view that is positioned absolute at the bottom of the screen. This view contains a text input. When the text input is focused, I want the bottom of the view to touch the top of the keyboard.
I've been messing around with KeyboardAvoidingView, but the keyboard keeps going over my view. Is it not possible to make this work with position absolute?
What other method can I try? Thanks!

Few days ago I have the same problem (although I have a complex view with TextInput as a child) and wanted not only the TextInput to be focused but the whole view to be "attached" to the keyboard. What's finally is working for me is the following code:
constructor(props) {
super(props);
this.paddingInput = new Animated.Value(0);
}
componentWillMount() {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillHideSub.remove();
}
keyboardWillShow = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: 60,
}).start();
};
keyboardWillHide = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: 0,
}).start();
};
render() {
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
[...]
<Animated.View style={{ marginBottom: this.paddingInput }}>
<TextTranslateInput />
</Animated.View>
</KeyboardAvoidingView>
);
}
where [..] you have other views.

Custom hook:
import { useRef, useEffect } from 'react';
import { Animated, Keyboard, KeyboardEvent } from 'react-native';
export const useKeyboardHeight = () => {
const keyboardHeight = useRef(new Animated.Value(0)).current;
useEffect(() => {
const keyboardWillShow = (e: KeyboardEvent) => {
Animated.timing(keyboardHeight, {
duration: e.duration,
toValue: e.endCoordinates.height,
useNativeDriver: true,
}).start();
};
const keyboardWillHide = (e: KeyboardEvent) => {
Animated.timing(keyboardHeight, {
duration: e.duration,
toValue: 0,
useNativeDriver: true,
}).start();
};
const keyboardWillShowSub = Keyboard.addListener(
'keyboardWillShow',
keyboardWillShow
);
const keyboardWillHideSub = Keyboard.addListener(
'keyboardWillHide',
keyboardWillHide
);
return () => {
keyboardWillHideSub.remove();
keyboardWillShowSub.remove();
};
}, [keyboardHeight]);
return keyboardHeight;
};

#jazzdle example works great! Thank you for that!
Just one addition - in keyboardWillShow method, one can add event.endCoordinates.height so paddingBottom is exact height as keyboard.
keyboardWillShow = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: event.endCoordinates.height,
}).start();
}

Using Functional Component. This works for both iOS and Android
useEffect(() => {
const keyboardVisibleListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
handleKeyboardVisible
);
const keyboardHiddenListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
handleKeyboardHidden
);
return () => {
keyboardHiddenListener.remove();
keyboardVisibleListener.remove();
};}, []);
const handleKeyboardVisible = (event) => {
Animated.timing(paddingInput, {
duration: event.duration,
toValue: 60,
useNativeDriver: false,
});};
const handleKeyboardHidden = (event: any) => {
Animated.timing(paddingInput, {
duration: event.duration,
toValue: 0,
useNativeDriver: false,
});};

React Native now supports an InputAccessoryView which can be used for exactly this purpose - even for anchored TextInputs.
Here's a specific example: https://github.com/facebook/react-native/blob/main/packages/rn-tester/js/examples/InputAccessoryView/InputAccessoryViewExample.js

You can use flexbox to bottom position the element. Here's simple example -
render() {
return (
<View style={styles.container}>
<View style={styles.top}/>
<View style={styles.bottom}>
<View style={styles.input}>
<TextInput/>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
top: {
flex: .8,
},
bottom: {
flex: .2,
},
input: {
width: 200,
},
});

Related

Make an image movable in React

We simply want to make an image movable around the screen and then snap back to the middle, we have our code below and that's working fine currently styled with a blue box that you can move around, but we're unable to figure out how we can amend the blue box into being our image that we have required in.
import React, { useRef } from "react";
import { Animated, PanResponder, StyleSheet, View, Image } from "react-native";
const Users = [{id:"1", uri: require('./assets/present.jpeg'), keyword: 'present1'}]
const DraggableView = () => {
const position = useRef(new Animated.ValueXY()).current;
console.log(pan)
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{
dx: pan.x, // x,y are Animated.Value
dy: pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(
pan, // Auto-multiplexed
{ toValue: { x: 0, y: 0 } } // Back to zero
).start();
},
});
return (
<View style={styles.container}>
<Animated.View
{...panResponder.panHandlers}
style={[pan.getLayout(), <Image source={require('./assets/present.jpeg')}/>}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
box: {
backgroundColor: "#61dafb",
width: 80,
height: 80,
borderRadius: 4,
},
});
export default DraggableView;
The <Image /> component should be a child of the <Animated.View /> component. Currently, you have it defined in the style prop which doesn't work.
const DraggableView = () => {
const position = useRef(new Animated.ValueXY()).current;
console.log(pan)
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{
dx: pan.x, // x,y are Animated.Value
dy: pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(
pan, // Auto-multiplexed
{ toValue: { x: 0, y: 0 } } // Back to zero
).start();
},
});
return (
<View style={styles.container}>
<Animated.View
{...panResponder.panHandlers}
style={pan.getLayout()}
>
<Image source={require('./assets/present.jpeg')}/>
</Animated.View>
</View>
);
};

How to reset value on click in React Native animated?

I have a progress bar screen inside navigation that I need to be reset every time user clicks on that specific route. Now it only plays animation when I go to that route for the first time. My question is: how to reset barWidth so that animation would play every time user clicks on specific route?
What have I tried?
I thought that the problem is that the component is not re-rendering and thats why value is not resetting, but it looks like the problem is that the width of the bar doesn't reset when user clicks on the screen after animation plays.
At first I've tried using useRef hook and later changed to simply setting the animated value to 0, but in both cases the bar's width didn't reset.
Code:
const { width } = Dimensions.get("screen");
const SettingsScreen = ({navigation}) => {
const isFocused = useIsFocused();
return (
<FlatList
contentContainerStyle={style.barContainer}
data={[1, 2, 3, 4, 5]}
keyExtractor={(_, index) => index.toString()}
renderItem={() => (
<ProgressBar isFocused={isFocused} navigation={navigation} />
)}
></FlatList>
);
};
const ProgressBar = ({ navigation, isFocused }) => {
//const barWidth = React.useRef(new Animated.Value(0)).current;
const barWidth = new Animated.Value(0);
console.log(barWidth);
const finalWidth = width / 2;
React.useEffect(() => {
const listener = navigation.addListener("focus", () => {
Animated.spring(barWidth, {
toValue: finalWidth,
bounciness: 10,
speed: 2,
useNativeDriver: false,
}).start();
});
return listener;
}, [navigation]);
return (
<View style={style.contentContainer}>
<Animated.View style={[style.progressBar, { width: barWidth }]} />
</View>
);
};
const style = StyleSheet.create({
contentContainer: {
flex: 1,
justifyContent: "center",
padding: 30,
},
barContainer: {
padding: 30,
},
progressBar: {
backgroundColor: "green",
width: width / 2,
height: 15,
borderRadius: 15,
},
});
The addListener function is Deprecated. try to use addEventListener instead.
also, why is it inside a const listener with the return listener?
as i see it you can write the useEffect like that:
React.useEffect(() => {
navigation.addEventListener("focus", () => {
Animated.spring(barWidth, {
toValue: finalWidth,
bounciness: 10,
speed: 2,
useNativeDriver: false,
}).start();
});
}, [navigation]);

React native updating the state using previous state behave differently

I have this simple react native app that when click on a button add a message to list and display the message for 2 second then the message get deleted from the list
import React, {
useEffect,
useRef,
useState,
} from 'react';
import {
Animated,
Button,
Text,
View,
} from 'react-native';
const getRandomMessage = () => {
const number = Math.trunc(Math.random() * 10000);
return 'Random message ' + number;
};
const Message = (props) => {
const opacity = useRef(new Animated.Value(0))
.current;
useEffect(() => {
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.delay(2000),
Animated.timing(opacity, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}),
]).start(() => {
props.onHide();
});
}, []);
return (
<Animated.View
style={{
opacity,
transform: [
{
translateY: opacity.interpolate({
inputRange: [0, 1],
outputRange: [-20, 0],
}),
},
],
margin: 10,
marginBottom: 5,
backgroundColor: 'white',
padding: 10,
borderRadius: 4,
shadowColor: 'black',
shadowOffset: {
width: 0,
height: 3,
},
shadowOpacity: 0.15,
shadowRadius: 5,
elevation: 6,
}}
>
<Text>{props.message}</Text>
</Animated.View>
);
};
export default () => {
const [messages, setMessages] = useState([]);
return (
<>
<View
style={{
position: 'absolute',
top: 45,
left: 0,
right: 0,
}}
>
{messages.map((message) => (
<Message
key={message}
message={message}
onHide={() => {
setMessages((messages) =>
messages.filter(
(currentMessage) =>
currentMessage !== message
)
);
}}
/>
))}
</View>
<Button
title="Add message"
onPress={() => {
const message = getRandomMessage();
setMessages([...messages, message]);
}}
/>
</>
);
};
if i change the code that delete the message from the list
from
setMessages((messages) =>
messages.filter(
(currentMessage) =>
currentMessage !== message
)
to this
const temp = messages.filter(currentMessage => currentMessage !== message);
setMessages(temp);
the app behave differently and the whole screen re-render
is there any explanation for this because it took me hours to figure out what is the problem with my code but i do not understand why it did work when I changed the state update function to use the previous state

Trying to give a fade animation to my image carousel, React-Native

I want to animate this image carousel in reactNative, but have no idea how to start. Read the documentation about animations but still really stuck, have no idea how to incorporate it in. I tried it this way but keep getting a big fat error. Help!
import React from 'react';
import {StyleSheet, View, ScrollView, Dimensions, Image, Animated} from 'react-native'
const DEVICE_WIDTH = Dimensions.get('window').width;
class BackgroundCarousel extends React.Component {
scrollRef = React.createRef();
constructor(props) {
super(props);
this.state = {
selectedIndex: 0,
opacity: new Animated.Value(0)
};
}
componentDidMount = () => {
Animated.timing(this.state.opacity , {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
setInterval(() => {
this.setState(
prev => ({ selectedIndex: prev.selectedIndex ===
this.props.images.length - 1 ? 0 : prev.selectedIndex +1 }),
() => {
this.scrollRef.current.scrollTo({
animated: true,
y: 0,
x: DEVICE_WIDTH * this.state.selectedIndex
});
}
);
}, 6000);
};
componentWillUnmount() {
clearInterval(this.setState);
}
render() {
const {images} = this.props
const {selectedIndex} = this.state
return (
<Animated.Image
onLoad={this.onLoad}
{...this.props}
style={[
{
opacity: this.state.opacity,
},
this.props.style,
]}
/>
<View style= {{height: "100%", width: "100%"}}>
{this.props.children}
<ScrollView
horizontal
pagingEnabled
scrollEnabled={false}
ref={this.scrollRef}
>
{images.map(image => (
<Image
key={image}
source={image}
style={styles.backgroundImage}
/>
))}
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create ({
backgroundImage: {
height: '100%',
width: DEVICE_WIDTH,
}
});
export default BackgroundCarousel;
Any help would be appreciated. Don't know where I'm going wrong. Basically trying to add a fade effect when my background carousel changes from image to image.
I have fixed your code and removed all errors, copy-paste it in https://snack.expo.io/ and give it some time to load.
Note: I have removed this.props.images for website demo, please change in your real project.
Working fade carousal: https://snack.expo.io/#rajrohityadav/fade-carosal
But I have not implemented this using React Animation.
import React from 'react';
import {StyleSheet, View, ScrollView, Dimensions, Image, Animated} from 'react-native'
const DEVICE_WIDTH = Dimensions.get('window').width;
export default class BackgroundCarousel extends React.Component {
scrollRef = React.createRef();
constructor(props) {
super(props);
this.state = {
selectedIndex: 0,
opacity: new Animated.Value(0)
};
}
componentDidMount = () => {
Animated.timing(this.state.opacity , {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
setInterval(() => {
this.setState(
prev => ({ selectedIndex: prev.selectedIndex ===
3 - 1 ? 0 : prev.selectedIndex +1 }),
() => {
this.scrollRef.current.scrollTo({
animated: true,
y: 0,
x: DEVICE_WIDTH * this.state.selectedIndex
});
}
);
}, 6000);
};
componentWillUnmount() {
clearInterval(this.setState);
}
render() {
const images =[
'https://image.shutterstock.com/image-vector/dragon-scream-vector-illustration-tshirt-260nw-1410107855.jpg','https://image.shutterstock.com/image-vector/dragon-head-vector-illustration-mascot-600w-1201914655.jpg',
'https://i.pinimg.com/474x/b7/1a/bb/b71abb6dd7678bbd14a1f56be5291747--dragon-illustration-samurai-tattoo.jpg']//this.props
const {selectedIndex} = this.state
return (
<>
<Animated.Image
onLoad={this.onLoad}
{...this.props}
style={[
{
opacity: this.state.opacity,
},
this.props.style,
]}
/>
<View style= {{height: "100%", width: "100%"}}>
{this.props.children}
<ScrollView
horizontal
pagingEnabled
scrollEnabled={false}
ref={this.scrollRef}
>
{images.map(image => (
<Image
key={image}
source={image}
style={styles.backgroundImage}
/>
))}
</ScrollView>
</View>
</>
)
}
}
const styles = StyleSheet.create ({
backgroundImage: {
height: '100%',
width: DEVICE_WIDTH,
}
});
You can also use a simple & optimized library react-native-fadecarousel and use it like this:
import React from 'react'
import { View, StyleSheet, Image } from 'react-native';
import FadeCarousel from 'react-native-fadecarousel';
const FadeCarouselScreen = () => {
const images = [
'https://image.shutterstock.com/image-vector/dragon-scream-vector-illustration-tshirt-260nw-1410107855.jpg',
'https://image.shutterstock.com/image-vector/dragon-head-vector-illustration-mascot-600w-1201914655.jpg',
'https://i.pinimg.com/474x/b7/1a/bb/b71abb6dd7678bbd14a1f56be5291747--dragon-illustration-samurai-tattoo.jpg'
];
return <FadeCarousel
loop
fadeAnimationDuration={1000}
autoPlay={{enable: true , delay: 1000 }}>
{
images.map((image, index) => {
return <View key={`slide ${index}`} style={styles.slideItem}>
<Image style={styles.image} resizeMethod="resize" resizeMode="cover" source={{ uri: image }}/>
</View>
})
}
</FadeCarousel>
}
const styles = StyleSheet.create({
image: {
width: "100%",
height: 300
},
slideItem: {
width: "100%",
height: 300,
justifyContent: "center",
alignContent: "center"
}
})
export default FadeCarouselScreen

How does "Animated.createAnimatedComponent" work?

In the Component ProgressBarAndroid, there are props indeterminable={Boolean} which show to a user an animation of what it's going on. I would like to do almost the same on ProgressViewIOS. So I tried to Animate it with Animated...
I saw on docs of Animated method called 'createAnimatedComponent' which they use to create Animated.View
I tried so to create another Animated (Native) Component but it doesn't work at all.
The animation should gradually raise fillValue to 20 % and continue with an original value from the media upload...
This is my Component
// ProgressBar.ios.js
// #flow
import { PropTypes } from 'react';
import Component from 'components/base/Component';
import { ProgressViewIOS, Animated } from 'react-native';
const AnimatedProgressViewIOS = Animated.createAnimatedComponent(ProgressViewIOS);
class ProgressBarIOS extends Component {
static propTypes = {
// Percentage (0 - 100)
fill: PropTypes.number.isRequired,
};
constructor(props, context: any) {
super(props, context);
this.state = {
fillValue: new Animated.Value(props.fill),
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.fill === 0) {
Animated.timing(this.state.fillValue, { toValue: 0.2, duration: 500 }).start();
} else if (nextProps.fill > 19) {
this.state.fillValue.setValue(nextProps.fill / 100);
}
}
shouldComponentUpdate(nextProps) {
return this.props.fill !== nextProps.fill;
}
render() {
return (
<AnimatedProgressViewIOS
style={{ alignSelf: 'stretch' }}
progress={this.state.fillValue} />
);
}
}
export default ProgressBarIOS;
EDIT: AnimatedComponent is used to modify style only. Props could be passed as animated value but remember it is not a number!
Animated.createAnimatedComponent can animate a number of different properties, however only some properties are supported using the native driver, fortunately it appears progress on ProgressViewIOS is one of them.
Here is a working implementation of an animated ProgressViewIOS.
import * as React from 'react';
import { View, SafeAreaView } from 'react-native';
import { ProgressViewIOS, Animated } from 'react-native';
const AnimatedProgressViewIOS = Animated.createAnimatedComponent(
ProgressViewIOS
);
export default function App() {
const value = React.useRef(new Animated.Value(0));
React.useEffect(() => {
Animated.loop(
Animated.timing(value.current, {
duration: 2000,
toValue: 1,
useNativeDriver: true,
})
).start();
}, []);
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<AnimatedProgressViewIOS
style={{ alignSelf: 'stretch' }}
progress={value.current}
/>
</View>
</SafeAreaView>
);
}
It's worth noting that ProgressViewIOS is now deprecated, but building your own progress view is very straight forward and requires just two Views with simple styling like this (expo snack):
import * as React from 'react';
import { View, SafeAreaView, StyleSheet, Button, Text } from 'react-native';
import { Animated } from 'react-native';
export default function App() {
const [progress, setProgress] = React.useState(() => Math.random());
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<AnimatedProgressView progress={progress} />
<Text style={{padding: 20, textAlign: 'center'}}>{Math.round(progress * 100)}%</Text>
<Button title="Animate" onPress={() => setProgress(Math.random())} />
</View>
</SafeAreaView>
);
}
function AnimatedProgressView({ progress, style }) {
const value = React.useRef(new Animated.Value(0));
const [width, setWidth] = React.useState(0);
React.useEffect(() => {
Animated.spring(value.current, { toValue: progress }).start();
}, [progress]);
return (
<View
style={[styles.track, style]}
onLayout={(event) => setWidth(event.nativeEvent.layout.width)}>
<Animated.View
style={[
styles.fill,
{
transform: [
{
translateX: value.current.interpolate({
inputRange: [0, 1],
outputRange: [-width, 0],
overflow: 'clamp',
}),
},
],
},
]}
/>
</View>
);
}
const styles = StyleSheet.create({
track: {
minHeight: 4,
borderRadius: 2,
overflow: 'hidden',
backgroundColor: '#ddd',
},
fill: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'blue',
},
});

Categories