Close bottom sheet modal only from top part in React Native - react-native

I created this bottom sheet modal component and it works just fine, the thing is that when it has a ScrollView inside, the scroll no longer works properly because it tries to close the modal. Is there any way to make the modal only be closed from the top part and the rest of the content be scrolled as normal?
export default (props: any) => {
const screenHeight = Dimensions.get('screen').height;
const panY = useRef(new Animated.Value(screenHeight)).current;
const resetPositionAnim = Animated.timing(panY, {
toValue: 0,
duration: 300,
useNativeDriver: true,
});
const closeAnim = Animated.timing(panY, {
toValue: screenHeight,
duration: 300,
useNativeDriver: true,
});
const translateY = panY.interpolate({
inputRange: [-1, 0, 1],
outputRange: [0, 0, 1],
});
const handleDismiss = () => closeAnim.start(props.onDismiss);
useEffect(() => {
resetPositionAnim.start();
}, [resetPositionAnim]);
const panResponders = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => false,
onPanResponderMove: Animated.event([null, { dy: panY }], {
useNativeDriver: false,
}),
onPanResponderRelease: (_, gs) => {
if (gs.dy > 0 && gs.vy > 2) {
return handleDismiss();
}
return resetPositionAnim.start();
},
}),
).current;
const insets = useSafeAreaInsets();
return (
<Modal animated animationType="fade" visible={props.visible} transparent onRequestClose={handleDismiss}>
<TouchableWithoutFeedback onPress={handleDismiss}>
<View style={styles.overlay}>
<Animated.View
style={{
...styles.container,
maxHeight: screenHeight - (insets.top || 100),
paddingBottom: insets.bottom,
transform: [{ translateY: translateY }],
}}
{...panResponders.panHandlers}
>
<View style={styles.sliderIndicatorRow}>
<View style={styles.sliderIndicator} />
</View>
<View style={[styles.sliderIndicatorRow, { justifyContent: 'flex-end', marginRight: 20 }]}>
<TouchableOpacity onPress={handleDismiss}>
<Feather name="x" size={16} color={colors.primaryText} />
</TouchableOpacity>
</View>
{props.children}
</Animated.View>
</View>
</TouchableWithoutFeedback>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
backgroundColor: 'rgba(0,0,0,0.2)',
flex: 1,
justifyContent: 'flex-end',
},
container: {
backgroundColor: 'white',
paddingTop: 12,
borderTopRightRadius: 30,
borderTopLeftRadius: 30,
minHeight: 200,
},
sliderIndicatorRow: {
flexDirection: 'row',
marginBottom: 4,
alignItems: 'center',
justifyContent: 'center',
},
sliderIndicator: {
backgroundColor: '#C4C4C4',
height: 6,
width: 75,
borderRadius: 100,
},
});

Related

Combined total for multiple slider react native with reanmiated

I need to do the following implementation in React Native:
http://jsfiddle.net/t8mzLuh4/3/
But I can't think of the most optimal way to do it, I have tried different ways, but they have not been optimal.
I am trying to implement a screen where there are 3 sliders, and I want to make the combined total of the 3 sliders never go over the maximum.
I don't care how it is implemented, it can be from a starting 0, and as soon as I change 1 slider, the remaining total available decreases or putting a slider beyond the maximum, decreases the values on the other sliders.
This is my current implementation of the slider with reanimated 2
const Slider = ({
label,
min,
value,
labelAmount,
colorAmount,
max,
currency,
onChange,
step,
}: SliderProps) => {
const isActive = useSharedValue(false);
const translationX = useSharedValue(((width - PADDING * 2) * 0) / max);
const amount = useSharedValue(value);
const formattedAmount = useSharedValue(
formatAsMoney(value, currency ?? '$')
);
const wrapper = (value: number) => {
formattedAmount.value = formatAsMoney(value, currency ?? '$');
};
useDerivedValue(() => {
runOnJS(wrapper)(amount.value);
});
const onSelect = (value: number) => {
'worklet';
runOnJS(onChange)(value);
};
const onGestureEvent = useAnimatedGestureHandler({
onStart: () => {
isActive.value = true;
},
onActive: (event) => {
translationX.value = interpolate(
event.x,
[0, width],
[0 - RADIUS, width - PADDING],
Extrapolate.CLAMP
);
amount.value =
Math.ceil(
interpolate(
translationX.value,
[0 - RADIUS, width - PADDING * 2 + RADIUS],
[min, max],
Extrapolate.CLAMP
) / step
) * step;
},
onEnd: () => {
isActive.value = false;
onSelect(amount.value);
},
});
const transform = useAnimatedStyle(() => {
const translateX = translationX.value;
return {
transform: [
{ translateX },
{ scale: withSpring(isActive.value ? 1 : 0.75) },
],
};
});
const barWidth = useAnimatedStyle(() => {
return {
width: translationX.value + RADIUS,
};
});
return (
<View>
<View
style={{
flexDirection: "row",
justifyContent="space-between",
alignItems="center"
}}
>
<Text>
{label}
</Text>
<ReText
text={formattedAmount}
style={[
styles.text,
{
fontSize: labelAmount === 'small' ? 18 : 22,
color: '#000',
},
]}
/>
</View>
<View style={styles.shadow}>
<View style={styles.container}>
<View style={styles.barContainer}>
<View
style={[
styles.bar,
{
backgroundColor: '#fff',
},
]}
/>
<Animated.View
style={[
styles.bar,
{
backgroundColor: 'green',
},
barWidth,
]}
/>
</View>
<PanGestureHandler {...{ onGestureEvent }}>
<Animated.View
style={[
StyleSheet.absoluteFillObject,
{
marginHorizontal: PADDING / 2,
},
]}
>
<Animated.View
style={[styles.cursor, transform, styles.shadow]}
/>
</Animated.View>
</PanGestureHandler>
</View>
<View
style={{
alignSelf: "flex-start",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%"
}}
>
<Text >
{formatAsMoney(min, currency ?? '$')}
</Text>
<Text>
{formatAsMoney(max, currency ?? '$')}
</Text>
</View>
</View>
</View>
);
};
const useStyles = StyleSheet.create({
shadow: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
alignItems: 'center',
},
container: {
height: HEIGHT,
width: width,
alignItems: 'center',
justifyContent: 'center',
},
barContainer: {
height: BAR_HEIGHT,
width: width - PADDING,
},
bar: {
borderRadius: BORDER_RADIUS,
...StyleSheet.absoluteFillObject,
},
cursor: {
height: RADIUS * 2,
width: RADIUS * 2,
borderRadius: RADIUS,
backgroundColor: 'white',
},
text: {
fontWeight: '700',
},

How to increase height of view using panresponder in react native?

I am trying to create two sibling views changing there height. When we drag a view then its height should increase and other height should decrease & vice varsa. I am using pan responder to do this but any other approach will also be great. Issue is when i drag then handle the view moves up or down but its height does not changes. Similarly other view also not changing its height.
const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
pan.setOffset({
x: pan.x._value,
y: pan.y._value
});
},
onPanResponderMove: Animated.event(
[
null,
{ dx: pan.x, dy: pan.y }
], { useNativeDriver: false }
),
onPanResponderRelease: () => {
pan.flattenOffset();
}
})
).current;
return <SafeAreaView style={{ flex: 1 }} forceInset={{ top: 'never', bottom: 'never' }}>
<Animated.View style={[{ backgroundColor: 'yellow', flex: 1 }]}>
</Animated.View>
<Animated.View style={[{
height: 100,
backgroundColor: 'skyblue'
}, {
transform: [{ translateY: pan.y }]
}]}>
<Animated.View style={[{ justifyContent: 'center', alignItems: 'center', paddingVertical: 5, backgroundColor: '#f0f3f7' }]}
{...panResponder.panHandlers}
>
<Block flex={false} color='#ccc' radius={10} style={{ height: 5, width: 35 }} />
</Animated.View>
</Animated.View>
</SafeAreaView>

Animation does not work properly in react native

I tried making a carousel by watching a tutorial but I cannot get it to work for an event driven animation. Instead of animating it just updates the position to new location.
This does not happen if I use only one type of animation for transition, mentioning just one value to transform rotate instead of passing an expression.
what it looks like
what it should look like
const cards = ["tomato", "teal", "pink"]
const alpha = Math.PI/6
const Trans = () => {
const value = React.useRef(new Animated.Value(0)).current
const [toggled, setToggled] = React.useState(false)
const animationFn = () => {
Animated.spring(value, {
toValue: 1,
friction: 10,
useNativeDriver: true
}).start()
setToggled(toggled => !toggled)
}
const rotateOpen = (rotate) => {
return value.interpolate({
inputRange: [0, 1],
outputRange: ['0rad', `${rotate}rad`]
})
}
const rotateClose = (rotate, maxValues) => {
return value.interpolate({
inputRange: [0, 1],
outputRange: [`${maxValues}rad`, `${rotate}rad`]
})
}
return(
<>
{cards.map((card, index) => {
const rotate = toggled ? (index - 1) * alpha : 0
const maxValues = (index-1) * alpha
return (
<Animated.View key={card} style={{transform: [
{translateY: -50},
{translateX: -100},
{rotate: !toggled ? rotateOpen(rotate) : rotateClose(rotate, maxValues) },
{translateX: 100},
], borderRadius: 15, position: 'absolute', backgroundColor: card, height: 100, width: 200}} />
)
})}
<View style={{paddingTop: 100}}>
<TouchableOpacity onPress={() => { animationFn() }}>
<Text style={{fontSize: 30}}> Animate </Text>
</TouchableOpacity>
</View>
</>
)
}
Your interpolation values shouldn't change between the open and close functions. The animation library knows that when you go from 0 to 1, you're rotating the block "out" and then when you go from 1 back to 0, you're applying the same interpolation in reverse
so this code appears to work correctly for me:
const Trans = () => {
const value = React.useRef(new Animated.Value(0)).current;
const [toggled, setToggled] = React.useState(false);
useEffect(() => {
Animated.spring(value, {
toValue: toggled ? 0 : 1,
friction: 10,
useNativeDriver: false,
}).start();
}, [toggled, value]);
return (
<>
{cards.map((card, index) => {
const rotate = (index - 1) * alpha;
return (
<Animated.View
key={card}
style={{
transform: [
{ translateY: -50 },
{ translateX: -100 },
{
rotate: value.interpolate({
inputRange: [0, 1],
outputRange: ['0rad', `${rotate}rad`],
}),
},
{ translateX: 100 },
],
borderRadius: 15,
position: 'absolute',
backgroundColor: card,
height: 100,
width: 200,
}}
/>
);
})}
<View style={{ paddingTop: 100 }}>
<TouchableOpacity
onPress={() => {
setToggled(!toggled);
}}>
<Text style={{ fontSize: 30 }}> Animate </Text>
</TouchableOpacity>
</View>
</>
);
};

scaling a react-native button with animated

I'm creating a touchable button in react native with an animation. When the button is pressed, it should scale down a little bit. When the pressure is released, it should scale back to normal.
This is my code:
export const TouchableButton = (props) => {
const { onPress, text, icon } = props
const animatedValue = new Animated.Value(0)
const animatedValueInterpolateScale = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.95]
})
const pressInHandler = () => {
Animated.timing(
animatedValue,
{
toValue: 1,
duration: 150
}
).start()
}
const pressOutHandler = () => {
Animated.timing(
animatedValue,
{
toValue: 0,
duration: 150
}
).start()
}
return (
<TouchableWithoutFeedback onPress={onPress} onPressIn={pressInHandler} onPressOut={pressOutHandler}>
<View style={{ alignItems: 'center' }}>
<Animated.View style={{ width: '100%', height: 40, borderRadius: 5, overflow: 'hidden', transform: [{ scaleX: animatedValueInterpolateScale }, { scaleY: animatedValueInterpolateScale }] }}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: Color.GrayLight }}>
<Text style={{ marginTop: 2.5, fontFamily: 'AlegreyaSans-Medium', fontSize: 15, color: Color.White }}>{text}</Text>
<View style={{ position: 'absolute', left: 12.5, top: 12.5 }}>
<Icon lib={icon.lib} icon={icon.icon} color={Color.White} size={15} />
</View>
</View>
</Animated.View>
</View>
</TouchableWithoutFeedback>
)
}
When the button is pressed, the animation in pressInHandler is started, and the scale is animated from 1 to 0.95. This works. But when I release the pressure (onPressOut is called), the scale snaps back to 1 without a smooth animation. It seems like pressOutHandler (and the animation in it) never is called.
I have another button with the same properties but instead of scaling I set the background color, and this works like it should.
Make it simple.
Note: ALWAYS USE useNativeDriver: true
const App = () => {
const animation = new Animated.Value(0);
const inputRange = [0, 1];
const outputRange = [1, 0.8];
const scale = animation.interpolate({inputRange, outputRange});
const onPressIn = () => {
Animated.spring(animation, {
toValue: 1,
useNativeDriver: true,
}).start();
};
const onPressOut = () => {
Animated.spring(animation, {
toValue: 0,
useNativeDriver: true,
}).start();
};
return (
<View style={styles.container}>
<Animated.View style={[styles.button, {transform: [{scale}]}]}>
<TouchableOpacity
style={styles.btn}
activeOpacity={1}
onPressIn={onPressIn}
onPressOut={onPressOut}>
<Text style={styles.btnText}>BUTTON</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
};
export default App;
const styles = StyleSheet.create({
container: {flex: 1, alignItems: 'center', justifyContent: 'center'},
button: {
height: 70,
width: 200,
backgroundColor: 'red',
marginBottom: 20,
borderRadius: 10,
},
btn: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
btnText: {
color: '#fff',
fontSize: 25,
},
});
Here is a pretty simple solution without any animations which looks almost as native (at least on iOS):
import React from "react"
import { Pressable, PressableProps, StyleProp, ViewStyle } from "react-native"
type TouchableButtonProps = PressableProps & {
scale?: number;
style?: StyleProp<ViewStyle>;
}
const PressableScale: React.FC<TouchableButtonProps> = ({ scale, style, children, ...otherProps }) => {
return (
<Pressable style={({ pressed }) => [style, { transform: [{ scale: pressed ? (scale ?? 0.98) : 1 }] }]} {...otherProps}>
{children}
</Pressable>
)
}
Usage:
<PressableScale style={{ flex: 1, justifyContent: 'center', alignContent: 'center', backgroundColor: 'black', padding: 50, borderRadius: 12 }}>
<Text style={{ color: 'white' }}>This is pressable button</Text>
</PressableScale>

React Native animated input text

I want to show a cancel button, on the focus TextInput animation.
I did the following code, but a cancel button does not display and follow the box when focused. It's only shown after the animation end.
And when cancel button displayed, it is not on the same line with textinput.
How do I fix this?
const { width } = Dimensions.get('window');
const PADDING = 16;
const SEARCH_FULL_WIDTH = width - PADDING * 2; //search_width when unfocused
const SEARCH_SHRINK_WIDTH = width - PADDING - 90; //search_width when focused
class Search extends React.Component {
constructor(props: IProps) {
super(props);
this.state = {
inputLength: new Animated.Value(SEARCH_FULL_WIDTH),
searchBarFocused: false,
}
}
private onFocus = () => {
Animated.timing(this.state.inputLength, {
toValue: SEARCH_SHRINK_WIDTH,
duration: 250,
}).start(() => this.setState({ searchBarFocused: true }));
}
private onBlur = () => {
Animated.timing(this.state.inputLength, {
toValue: SEARCH_FULL_WIDTH,
duration: 250,
}).start(() => this.setState({ searchBarFocused: false }));
}
<View style={styles.searchContainer}>
<Animated.View style={[
styles.search,
{
width: this.state.inputLength,
position: 'absolute',
left: 16,
alignSelf: 'center'
},
searchBarFocused === true ? undefined : { justifyContent: 'center' }
]}>
<Image source={searchIcon} style={styles.image} />
<TextInput
style={styles.searchInput}
....
onBlur={this.onBlur}
onFocus={this.onFocus}
/>
</Animated.View>
{searchBarFocused &&
<Touchable style={styles.cancelSearch} onPress={this.cancelSearch}>
<Text style={styles.cancelSearchText}>Cancel</Text>
</Touchable>
}
</View>
const styles = StyleSheet.create({
searchContainer: {
flexDirection: 'row',
height: 72,
borderBottomColor: SOLITUDE_COLOR,
},
search: {
flex: 1,
flexDirection: 'row',
height: 40,
borderRadius: 6,
},
cancelSearch: {
marginHorizontal: 16,
textAlign: 'center',
justifyContent: 'center'
}
});
gif: when unfocus and focused
Here is a slightly modified version of your code.
import React from "react";
import {
Dimensions,
View,
Animated,
TextInput,
TouchableOpacity,
StyleSheet,
} from "react-native";
const { width } = Dimensions.get("window");
const PADDING = 16;
const SEARCH_FULL_WIDTH = width - PADDING * 2; //search_width when unfocused
const SEARCH_SHRINK_WIDTH = width - PADDING - 90; //search_width when focused
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
inputLength: new Animated.Value(SEARCH_FULL_WIDTH),
cancelPosition: new Animated.Value(0),
opacity: new Animated.Value(0),
searchBarFocused: false
};
}
onFocus = () => {
Animated.parallel([
Animated.timing(this.state.inputLength, {
toValue: SEARCH_SHRINK_WIDTH,
duration: 250
}),
Animated.timing(this.state.cancelPosition, {
toValue: 16,
duration: 400
}),
Animated.timing(this.state.opacity, {
toValue: 1,
duration: 250
})
]).start();
};
onBlur = () => {
Animated.parallel([
Animated.timing(this.state.inputLength, {
toValue: SEARCH_FULL_WIDTH,
duration: 250
}),
Animated.timing(this.state.cancelPosition, {
toValue: 0,
duration: 250
}),
Animated.timing(this.state.opacity, {
toValue: 0,
duration: 250
})
]).start();
};
render() {
const { searchBarFocused } = this.state;
return (
<View style={styles.searchContainer}>
<Animated.View
style={[
styles.search,
{
width: this.state.inputLength,
position: "absolute",
left: 16,
alignSelf: "center"
},
searchBarFocused === true ? undefined : { justifyContent: "center" }
]}
>
<TextInput
style={styles.searchInput}
onBlur={this.onBlur}
onFocus={this.onFocus}
placeholder="Type something"
/>
</Animated.View>
<AnimatedTouchable
style={[styles.cancelSearch, { right: this.state.cancelPosition }]}
onPress={() => null}
>
<Animated.Text
style={[styles.cancelSearchText, { opacity: this.state.opacity }]}
>
Cancel
</Animated.Text>
</AnimatedTouchable>
</View>
);
}
}
const styles = StyleSheet.create({
searchContainer: {
flexDirection: "row",
height: 72,
borderBottomColor: "#00000033",
paddingTop: 100
},
search: {
flex: 1,
flexDirection: "row",
height: 40,
borderRadius: 6,
backgroundColor: "red"
},
cancelSearch: {
position: "absolute",
marginHorizontal: 16,
textAlign: "center",
justifyContent: "center",
alignSelf: "center"
}
});
You're setting searchBarFocused only after your animation completes. Since the cancel button is conditionally rendered based on searchBarFocused, it only appears at the end of the animation.