Combined total for multiple slider react native with reanmiated - react-native

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',
},

Related

Close bottom sheet modal only from top part in 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,
},
});

How to implement swipeable in react native gesture handler

I want to implement a swipe to delete feature on flatlist data. I can get the swipe to work, but it only registers after the touch input is lifted. When I start dragging, the card does not initially drag, but it swipes after I lift the input. How can I make it so it starts dragging when I start moving the card?
Current Code:
export default class AppleStyleSwipeableRow extends Component {
private renderRightAction = (x: number, dragX) => {
const trans = dragX.interpolate({
inputRange: [0, 1],
outputRange: [x, 0],
extrapolate: "clamp",
});
const pressHandler = () => {
this.close();
Alert.alert("hi");
};
return (
<Animated.View
style={{
flex: 1,
borderRadius: 15,
height: 120,
transform: [{ translateX: trans }],
}}
>
<RectButton
style={[
styles.rightAction,
{ backgroundColor: "transparent", height: 50 },
]}
onPress={pressHandler}
>
<SquircleView
style={StyleSheet.absoluteFill}
squircleParams={{
cornerSmoothing: 0.6,
cornerRadius: 15,
fillColor: "#FF3B30",
}}
>
<Image
style={{
width: 17.37 * 1.5,
height: 19.66 * 1.5,
justifyContent: "center",
alignSelf: "center",
top: 40,
right: 3.5,
}}
source={require("../../assets/trash.fill.png")}
></Image>
</SquircleView>
</RectButton>
</Animated.View>
);
};
private renderRightActions = (
progress: Animated.AnimatedInterpolation,
_dragAnimatedValue: Animated.AnimatedInterpolation
) => (
<View
style={{
width: 90,
flexDirection: I18nManager.isRTL ? "row-reverse" : "row",
}}
>
{this.renderRightAction(90, progress)}
</View>
);
private swipeableRow?: Swipeable;
private updateRef = (ref: Swipeable) => {
this.swipeableRow = ref;
};
private close = () => {
this.swipeableRow?.close();
};
render() {
const { children } = this.props;
return (
<Swipeable
containerStyle={{ borderRadius: 15 }}
childrenContainerStyle={{ backgroundColor: "white", borderRadius: 15 }}
ref={this.updateRef}
friction={3}
enableTrackpadTwoFingerGesture
rightThreshold={40}
renderRightActions={this.renderRightActions}
>
{children}
</Swipeable>
);
}
}
const styles = StyleSheet.create({
actionText: {
color: "white",
fontSize: 16,
backgroundColor: "transparent",
padding: 10,
},
rightAction: {
alignItems: "center",
flex: 1,
justifyContent: "center",
left: 10,
},
});
ScreenA.tsx
const RenderItem = ({ item }) => {
return (
<View style={{ height: 120, width: W_WIDTH * 0.9, zIndex: -100 }}>
<Image
source={require("../../assets/pin.png")}
style={{
position: "absolute",
width: 40,
height: 40,
}}
/>
<Text
style={{
fontSize: 22,
paddingRight: 16,
color: "black",
fontFamily: "Medium",
left: 45,
top: 6,
}}
>
Foo
</Text>
</View>
);
};
const ScreenA = () => {
const SwipeableRow = ({ item }) => {
return (
<RectButton
style={{
width: W_WIDTH * 0.9,
height: 120,
alignItems: "center",
backgroundColor: "#f3f2f8",
borderRadius: 10,
marginHorizontal: 20,
marginTop: 20,
}}
onPress={() =>
navigation.navigate("ScreenB")
}
>
<AppleStyleSwipeableRow>
<RenderItem item={item} />
</AppleStyleSwipeableRow>
</RectButton>
);
};
return (
<StatusBar style={colorScheme == "dark" ? "light" : "dark"} />
<ScrollView
style={[
styles.container,
{
backgroundColor: colorScheme == "dark" ? "black" : "white",
},
]}
contentInsetAdjustmentBehavior="automatic"
keyboardDismissMode="on-drag"
>
<FlatList
data={bookmarks}
keyExtractor={(item) => item.country}
renderItem={({ item }) => <SwipeableRow item={item} />}
// renderItem={renderItem}
showsVerticalScrollIndicator={false}
/>
</ScrollView>
);
};
}
Does your RectButton have an onPressIn prop? That's what you'll need to use - if it doesn't have it, switch it out for a Pressable or other component with this prop.

How to scroll through an Image Carousel in React-Native expo using the Volume Buttons?

I am building an app for sheet music. I want to scroll through the images using the volume buttons. Why? Because ill modify a hands-free cable later to change the "volume" using a foot button. This is so the musician never leaves his instrument.
I used this snak to create an image carousel using expo. Now I want to incorporate the volume buttons.
CODE FROM THE SNAK:
import React, { Component } from 'react'
import { Animated, View, StyleSheet, Image, Dimensions, ScrollView , Text} from 'react-native'
const deviceWidth = Dimensions.get('window').width
const FIXED_BAR_WIDTH = 280
const BAR_SPACE = 10
const images = [
'https://s-media-cache-ak0.pinimg.com/originals/ee/51/39/ee5139157407967591081ee04723259a.png',
'https://s-media-cache-ak0.pinimg.com/originals/40/4f/83/404f83e93175630e77bc29b3fe727cbe.jpg',
'https://s-media-cache-ak0.pinimg.com/originals/8d/1a/da/8d1adab145a2d606c85e339873b9bb0e.jpg',
]
export default class App extends Component {
numItems = images.length
itemWidth = (FIXED_BAR_WIDTH / this.numItems) - ((this.numItems - 1) * BAR_SPACE)
animVal = new Animated.Value(0)
render() {
let imageArray = []
let barArray = []
images.forEach((image, i) => {
console.log(image, i)
const thisImage = (
<Image
key={`image${i}`}
source={{uri: image}}
style={{ width: deviceWidth }}
/>
)
imageArray.push(thisImage)
const scrollBarVal = this.animVal.interpolate({
inputRange: [deviceWidth * (i - 1), deviceWidth * (i + 1)],
outputRange: [-this.itemWidth, this.itemWidth],
extrapolate: 'clamp',
})
const thisBar = (
<View
key={`bar${i}`}
style={[
styles.track,
{
width: this.itemWidth,
marginLeft: i === 0 ? 0 : BAR_SPACE,
},
]}
>
<Animated.View
style={[
styles.bar,
{
width: this.itemWidth,
transform: [
{ translateX: scrollBarVal },
],
},
]}
/>
</View>
)
barArray.push(thisBar)
})
return (
<View
style={styles.container}
flex={1}
>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
scrollEventThrottle={10}
pagingEnabled
onScroll={
Animated.event(
[{ nativeEvent: { contentOffset: { x: this.animVal } } }]
)
}
>
{imageArray}
<View
style={styles.skip}
>
<Text style={{backgroundColor: '#fff',color:"#F44",textAlign:"center",alignItems: 'center',
justifyContent: 'center',}}>skip</Text>
</View>
</ScrollView>
<View
style={styles.barContainer}
>
{barArray}
</View>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
barContainer: {
position: 'absolute',
zIndex: 2,
bottom: 40,
flexDirection: 'row',
},
skip: {
position: 'absolute',
zIndex: 2,
bottom: 80,
flexDirection: 'row',
},
track: {
backgroundColor: '#ccc',
overflow: 'hidden',
height: 2,
},
bar: {
backgroundColor: '#5294d6',
height: 2,
position: 'absolute',
left: 0,
top: 0,
},
})
Requirements:
it must work with EXPO

"Can't perform a React state update on an unmounted component" Follow by continuous "TYPE ERORR: null is not an object"

The app I'm working on is a multi-game app (a bird game, mole game, and minesweeper). The start screen is the menu. It gives me a cycle warning on this screen. When I tap to start the mole game, it plays naturally, but the moment I press the back button to go back to the menu, I receive errors: "Can't perform a React state update on an unmounted component" error follow by continuous "TYPE ERORR: null is not an object".
Screenshots:
Cycle warning
Null is not an object
Node.js errors
Mole Game's App.js
import {
View,
StyleSheet,
Image,
SafeAreaView,
Text,
TouchableWithoutFeedback
} from 'react-native';
import Images from './assets/Images';
import Constants from './Constants';
import Mole from './Mole';
import GameOver from './GameOver';
import Clear from './Clear';
import Pause from './Pause';
const DEFAULT_TIME = 20;
const DEFAULT_STATE = {
level: 1,
score: 0,
time: DEFAULT_TIME,
cleared: false,
paused: false,
gameover: false,
health: 100
}
export default class MoleGame extends Component {
constructor(props) {
super(props);
this.moles = [];
this.state = DEFAULT_STATE;
this.molesPopping = 0;
this.interval = null;
this.timeInterval = null;
}
componentDidMount = () => {
this.setupTicks(DEFAULT_STATE, this.pause);
}
setupTicks = () => {
let speed = 750 - (this.state.level * 50);
if (speed < 350) {
speed = 350;
}
this.interval = setInterval(this.popRandomMole, speed);
this.timeInterval = setInterval(this.timerTick, 1000);
}
reset = () => {
this.molesPopping = 0;
this.setState(DEFAULT_STATE, this.setupTicks)
}
pause = () => {
if (this.interval) clearInterval(this.interval);
if (this.timeInterval) clearInterval(this.timeInterval);
this.setState({
paused: true
});
}
resume = () => {
this.molesPopping = 0;
this.setState({
paused: false
}, this.setupTicks);
}
nextLevel = () => {
this.molesPopping = 0;
this.setState({
level: this.state.level + 1,
cleared: false,
gameover: false,
time: DEFAULT_TIME
}, this.setupTicks)
}
timerTick = () => {
if (this.state.time === 0) {
clearInterval(this.interval);
clearInterval(this.timeInterval);
this.setState({
cleared: true
})
} else {
this.setState({
time: this.state.time - 1
})
}
}
gameOver = () => {
clearInterval(this.interval);
clearInterval(this.timerInterval);
this.setState({
gameover: true
})
}
onDamage = () => {
if (this.state.cleared || this.state.gameOver || this.state.paused) {
return;
}
let targetHealth = this.state.health - 10 < 0 ? 0 : this.state.health - 20;
this.setState({
health: targetHealth
});
if (targetHealth <= 0) {
this.gameOver();
}
}
onHeal = () => {
let targetHealth = this.state.health + 10 > 100 ? 100 : this.state.health + 10
this.setState({
health: targetHealth
})
}
onScore = () => {
this.setState({
score: this.state.score + 1
})
}
randomBetween = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
onFinishPopping = (index) => {
this.molesPopping -= 1;
}
popRandomMole = () => {
if (this.moles.length != 12) {
return;
}
let randomIndex = this.randomBetween(0, 11);
if (!this.moles[randomIndex].isPopping && this.molesPopping < 3) {
this.molesPopping += 1;
this.moles[randomIndex].pop();
}
}
render() {
let healthBarWidth = (Constants.MAX_WIDTH - Constants.XR * 100 - Constants.XR * 60 - Constants.XR * 6) * this.state.health / 100;
return (
<View style={styles.container}>
<Image style={styles.backgroundImage} resizeMode="stretch" source={Images.background} />
<View style={styles.topPanel}>
<SafeAreaView>
<View style={styles.statsContainer}>
<View style={styles.stats}>
<View style={styles.levelContainer}>
<Text style={styles.levelTitle}>Level</Text>
<Text style={styles.levelNumber}>{this.state.level}</Text>
</View>
</View>
<View style={styles.stats}>
<View style={styles.timeBar}>
<Text style={styles.timeNumber}>{this.state.time}</Text>
</View>
<Image style={styles.timeIcon} resizeMode="stretch" source={Images.timeIcon} />
</View>
<View style={styles.stats}>
<View style={styles.scoreBar}>
<Text style={styles.scoreNumber}>{this.state.score}</Text>
</View>
<Image style={styles.scoreIcon} resizeMode="stretch" source={Images.scoreIcon} />
</View>
<View style={styles.stats}>
<TouchableWithoutFeedback onPress={this.pause}>
<View style={styles.pauseButton}>
<Image style={styles.pauseButtonIcon} resizeMode="stretch" source={Images.pauseIcon} />
</View>
</TouchableWithoutFeedback>
</View>
</View>
<View style={styles.healthBarContainer}>
<View style={styles.healthBar}>
<View style={[styles.healthBarInner, { width: healthBarWidth }]} />
</View>
<Image style={styles.healthIcon} resizeMode="stretch" source={Images.healthIcon} />
</View>
</SafeAreaView>
</View>
<View style={styles.playArea}>
{Array.apply(null, Array(4)).map((el, rowIdx) => {
return (
<View style={styles.playRow} key={rowIdx}>
{Array.apply(null, Array(3)).map((el, colIdx) => {
let moleIdx = (rowIdx * 3) + colIdx;
return (
<View style={styles.playCell} key={colIdx}>
<Mole
index={moleIdx}
onDamage={this.onDamage}
onHeal={this.onHeal}
onFinishPopping={this.onFinishPopping}
onScore={this.onScore}
ref={(ref) => { this.moles[moleIdx] = ref }}
/>
</View>
)
})}
</View>
)
})}
</View>
{this.state.cleared && <Clear onReset={this.reset} onNextLevel={this.nextLevel} level={this.state.level} score={this.state.score} />}
{this.state.gameover && <GameOver onReset={this.reset} level={this.state.level} score={this.state.score} />}
{this.state.paused && <Pause onReset={this.reset} onResume={this.resume} />}
</View>
)
}
};
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column'
},
backgroundImage: {
width: Constants.MAX_WIDTH,
height: Constants.MAX_HEIGHT,
position: 'absolute'
},
topPanel: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: Constants.YR * 250,
flexDirection: 'column'
},
statsContainer: {
width: Constants.MAX_WIDTH,
height: Constants.YR * 120,
flexDirection: 'row'
},
stats: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
pauseButton: {
width: Constants.YR * 50,
height: Constants.YR * 50,
backgroundColor: 'black',
borderColor: 'white',
borderWidth: 3,
borderRadius: 10,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
pauseButtonIcon: {
width: Constants.YR * 25,
height: Constants.YR * 25,
},
levelContainer: {
width: Constants.YR * 80,
height: Constants.YR * 80,
backgroundColor: '#ff1a1a',
borderColor: 'white',
borderWidth: 3,
borderRadius: 10,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
levelTitle: {
fontSize: 21,
color: 'white',
fontFamily: 'LilitaOne'
},
levelNumber: {
fontSize: 17,
color: 'white',
fontFamily: 'LilitaOne'
},
scoreIcon: {
position: 'absolute',
left: 0,
width: Constants.YR * 40,
height: Constants.YR * 40,
},
scoreBar: {
height: Constants.YR * 25,
position: 'absolute',
left: 20,
right: 5,
backgroundColor: 'white',
borderRadius: 13,
justifyContent: 'center',
alignItems: 'center'
},
scoreNumber: {
fontSize: 17,
color: 'black',
fontFamily: 'LilitaOne',
},
timeIcon: {
position: 'absolute',
left: 0,
width: Constants.YR * 40,
height: Constants.YR * 40,
},
timeBar: {
height: Constants.YR * 25,
position: 'absolute',
left: 20,
right: 5,
backgroundColor: 'white',
borderRadius: 13,
justifyContent: 'center',
alignItems: 'center'
},
timeNumber: {
fontSize: 17,
color: 'black',
fontFamily: 'LilitaOne',
},
healthBarContainer: {
height: Constants.YR * 40,
width: Constants.MAX_WIDTH - Constants.XR * 120,
marginLeft: Constants.XR * 60
},
healthIcon: {
position: 'absolute',
top: 0,
left: 0,
width: Constants.YR * 46,
height: Constants.YR * 40,
},
healthBar: {
height: Constants.YR * 20,
width: Constants.MAX_WIDTH - Constants.XR * 100 - Constants.XR * 60,
marginLeft: Constants.XR * 40,
marginTop: Constants.YR * 10,
backgroundColor: 'white',
borderRadius: Constants.YR * 10
},
healthBarInner: {
position: 'absolute',
backgroundColor: '#ff1a1a',
left: Constants.XR * 3,
top: Constants.YR * 3,
bottom: Constants.YR * 3,
borderRadius: Constants.YR * 8
},
playArea: {
width: Constants.MAX_WIDTH,
marginTop: Constants.YR * 250,
height: Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112,
flexDirection: 'column',
},
playRow: {
height: (Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112) / 4,
width: Constants.MAX_WIDTH,
flexDirection: 'row',
},
playCell: {
width: Constants.MAX_WIDTH / 3,
height: (Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112) / 4,
alignItems: 'center'
}
});
Mole.js
import { View, StyleSheet, Button, Image, TouchableWithoutFeedback } from 'react-native';
import Images from './assets/Images';
import SpriteSheet from 'rn-sprite-sheet';
//import Constants from './Constants';
export default class Mole extends Component {
constructor (props) {
super(props);
this.mole = null;
this.actionTimeout = null;
this.isPopping = false;
this.isWacked = false;
this.isHealing = false;
this.isAttacking = false;
this.isFeisty = false;
}
pop =()=>{
this.isWacked = false;
this.isPopping = true;
this.isAttacking = false;
this.isFeisty = Math.random() < 0.4;
if(!this.isFeisty){
this.isHealing = Math.random() < 0.12;
}
if(this.isHealing){
this.mole.play({
type: "heal",
onFinish : ()=>{
this.actionTimeout = setTimeout(()=>{
this.mole.play({
type : "hide",
fps: 24,
onFinish: ()=>{
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}, 1000);
}
})
}
else{
this.mole.play({
type : "appear",
fps: 24,
onFinish: ()=>{
if (this.isFeisty){
this.actionTimeout = setTimeout(() => {
this.isAttacking = true;
this.props.onDamage();
this.mole.play({
type: "attack",
fps: 12,
onFinish: () => {
this.mole.play({
type: "hide",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}
})
}, 1000)
}
else{
this.actionTimeout = setTimeout(()=>{
this.mole.play({
type : "hide",
fps: 24,
onFinish: ()=>{
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}, 1000);
}
}
})
}
}
whack = ()=>{
if(!this.isPopping || this.isWacked || this.isAttacking){
return;
}
if (this.actionTimeout){
clearTimeout(this.actionTimeout);
}
this.isWacked = true;
this.props.onScore ();
if( this.isHealing){
this.props.onHeal();
}
this.mole.play({
type: "dizzy",
fps: 24,
onFinish: () => {
this.mole.play({
type: "faint",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}
})
}
render() {
return (
<View style= {styles.container}>
<SpriteSheet
ref= {ref=> {this.mole = ref}}
source = {Images.sprites}
columns = {6}
rows = {8}
width = {100}
animations = {{
idle: [0],
appear: [1,2,3,4],
hide: [4,3,2,1,0],
dizzy : [36,37,38],
faint: [42,43,44,0],
attack: [11,12,13,14,15,16],
heal: [24,25,26,27,28,29,30,31,32,33]
}} />
<TouchableWithoutFeedback onPress= {this.whack} style= {{position: 'absolute', top: 0 , bottom:0, left:0, right:0 }}>
<View style= {{position: 'absolute', top: 0 , bottom:0, left:0, right:0 }} />
</TouchableWithoutFeedback>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1
}
})
index.js (Navigation)
import React from "react";
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import MoleGame from './Game1/App.js'
import BirdGame from './Game2/App.js'
import HomeScreen from './Game3/screens/HomeScreen.js'
import GameScreen from './Game3/screens/GameScreen.js'
import Home from './Home.js'
const Stack = createStackNavigator();
export default App = () => {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false, gestureEnabled: false, }} >
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="MoleGame" component={MoleGame} />
<Stack.Screen name="BirdGame" component={BirdGame} />
<Stack.Screen name="MineSweeperHome" component={HomeScreen} />
<Stack.Screen name="MineSweeperGame" component={GameScreen} />
</Stack.Navigator>
</NavigationContainer>
);
};
Home.js
import { View, Text, TouchableOpacity, Dimensions, StyleSheet } from 'react-native';
const GameButton = ({ label, onPress, style }) => {
return (
<TouchableOpacity
style={style}
onPress={onPress}
>
<Text style={styles.text}>{label}</Text>
</TouchableOpacity>
);
};
export default Home = ({ navigation }) => {
return (
<View style={styles.container}>
<View style={styles.welcome}>
<Text style={styles.text}> Welcome! </Text>
</View>
<View style={styles.buttonContainer}>
<GameButton
label={'Play Bird Game'}
onPress={() => navigation.navigate('BirdGame')}
style={styles.gameButton}
/>
<GameButton
label={'Play Mole Game'}
onPress={() => navigation.navigate('MoleGame')}
style={{ ...styles.gameButton, backgroundColor: 'green' }}
/>
<GameButton
label={'Play MineSweeper Game'}
onPress={() => navigation.navigate('MineSweeperHome')}
style={{ ...styles.gameButton, backgroundColor: 'grey' }}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFCB43'
},
welcome: {
alignItems: 'center',
justifyContent: 'center',
padding: 10,
marginTop: 80,
},
buttonContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-evenly'
},
gameButton: {
height: 70,
width: Dimensions.get("screen").width / 1.4,
backgroundColor: 'red',
borderColor: 'red',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
borderWidth: 2,
borderColor: 'black',
},
text: {
fontSize: 22,
}
})
Some context and notices
The project was started on my friend's mac machine. According to him, he doesn't receive those errors. And he also was able to build the app into an .apk for Android
We are both using the same node.js version (15.11.0)
There are times when my metro fails to run, and when that happens, the app runs without any errors.
I have tried to build it into an .apk, but the build failed.
Before the app was combined into 3 games. They were individual games. The mole game did run and was built successfully into an .apk.
The mole game was forked from here The code is pretty much the same.
UPDATE :
After adding
componentWillUnmount(){
clearInterval(this.interval);
clearInterval(this.timerInterval);
}
to the App.js. I no longer get continuous "TypeError: null is not an object (evaluating '_this.moles[randomIndex].isPopping')". Unfortunately, I still get TypeError: null is not an object (evaluating '_this.moles.play) error. This happens when I go back to the main menu. I've tried adding clearTimeout(this.actionTimeout) on mole.js, but that didn't give any effect.
Screenshot :
TypeError: null is not an object (evaluating '_this.moles.play)
The setInterval() function is commonly used to set a delay for functions that are executed again and again, such as animations. You can cancel the interval using clearInterval().
When you navigate from MoleGame to Home, the MoleGame route is popped off the navigation stack and its component is unmounted. But the intervals from the setupTicks method are still executing, and trying to set state on the MoleGame component and to access this (neither of which are possible).
Try clearInterval on componentWillUnmount to stop the intervals that are set in the setupTicks method.
// App.js
export default class MoleGame extends Component {
constructor(props) {
super(props);
this.interval = null;
this.timeInterval = null;
...
}
componentDidMount = () => {
this.setupTicks(DEFAULT_STATE, this.pause);
}
componentWillUnmount() {
clearInterval(this.interval);
clearInterval(this.timeInterval);
}
}
Similarly in Mole.js you would have to handle the same scenario for any setTimeout()s you have by using clearTimeout()
Further reading: Plugging memory leaks in your app
By adding:
componentWillUnmount(){
clearInterval(this.interval);
clearInterval(this.timerInterval);
}
it fixed the continuous TypeError: null is not an object (evaluating '_this.moles[randomIndex].isPopping')
To remove the "TypeError: null is not an object (evaluating '_this.moles.play)" error, I had to put this.mole.play into a variable within Mole.js because this no longer refer to this.mole.play when I go back to the menu. There is still memory leak warnings since it is finishing the animation as I go to the menu, but there are no more critical errors.

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>