I am trying to auto-scroll between four images using the useRef hook which identifies the target using a state value called 'selected'.
I am getting unexpected behaviour in that the auto-scroll is erratic, usually leaving out the third image it should be scrolling to.
const [selected, setSelected] = useState(0);
const scrollRef = useRef(null);
const setSelectedIndex = e => {
const viewSize = e.nativeEvent.layoutMeasurement.width;
const contentOffset = e.nativeEvent.contentOffset.x;
const selectedIndex = Math.floor(contentOffset / viewSize);
setSelected(selectedIndex);
}
useEffect(() => {
setInterval(() => {
setSelected(prev => prev + 1);
scrollRef.current.scrollTo({
animated: true,
y: 0,
x: DEVICE_WIDTH * selected
});
}, 10000);
}, [selected]);
return (
<View style={{ height: '100%', width: '100%' }}>
<ScrollView
horizontal
pagingEnabled
onMomentumScrollEnd={setSelectedIndex}
ref={scrollRef}
>
{images.map(image => (
<Image
key={image}
source={image}
style={styles.backgroundImage}
/>
))}
</ScrollView>
<View style={styles.circleDiv}>
{
images.map((img, i) => (
<View
key={img}
style={[styles.whiteCircle, { opacity: i === selected ? 0.5 : 1 }]}
/>
))
}
</View>
</View>
);
If it helps, I have built it using componentDidMount which works as expected.
scrollRef = createRef();
componentDidMount = () => {
setInterval(() => {
this.setState({
prev => ({ selected: prev.selected + 1 }),
() => {
this.scrollRef.current.scrollTo({
animated: true,
y: 0,
x: DEVICE_WIDTH * this.state.selected
});
}
});
}, 5000);
}
try like this,
use useRef() instead of createRef()
return new state from setSelected arrow function
use setTimeout instead of setInterval
const scrollRef = useRef()
useEffect(() => {
setTimeout(() => {
setSelected((prev) => (
prev == slidesArray.length - 1 ? 0 : prev + 1
))
if (scrollRef.current) {
scrollRef.current.scrollTo({
animated: true,
y: 0,
x: DEVICE_WIDTH * (selected +1)
});
}
}, 5000);
}, [selected])
Related
How can I do a similar oval scroll?
What can I use for this?
Based on the assumption that you want something like this, I wrote a simple example
If someday the link turns out to be broken, below I attach the code additionally
import React, { useCallback, useState, useRef } from "react";
import {
FlatList,
Text,
View,
StyleSheet,
Dimensions,
Animated
} from "react-native";
const { height } = Dimensions.get("window");
const screenMiddle = height / 2;
const itemScaleOffset = height / 3;
const DATA = new Array(20).fill(0).map((...args) => ({
id: args[1],
title: args[1]
}));
// args[1] is an index, just I hate warnings
const Item = ({ title, offsetY }) => {
const [scrollEdges, setScrollEdges] = useState({
top: 0,
middle: 0,
bottom: 0
});
const onLayout = useCallback(
({
nativeEvent: {
layout: { top, height }
}
}) =>
setScrollEdges((edges) => ({
...edges,
top: top - itemScaleOffset - screenMiddle,
middle: top + height / 2 - screenMiddle,
bottom: top + height + itemScaleOffset - screenMiddle
})),
[]
);
const scale = offsetY.interpolate({
inputRange: [scrollEdges.top, scrollEdges.middle, scrollEdges.bottom],
outputRange: [0.66, 1, 0.66],
extrapolate: "clamp"
});
return (
<Animated.View
onLayout={onLayout}
style={[
{
transform: [
{
scale
}
]
},
styles.item
]}
>
<Text style={styles.title}>{title}</Text>
</Animated.View>
);
};
const keyExtractor = ({ id }) => id.toString();
const App = () => {
const offsetY = useRef(new Animated.Value(0)).current;
const renderItem = useCallback(
({ item: { title } }) => <Item title={title} offsetY={offsetY} />,
[offsetY]
);
return (
<View style={styles.app}>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={keyExtractor}
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: offsetY
}
}
}
],
{
useNativeDriver: false
}
)}
/>
</View>
);
};
const styles = StyleSheet.create({
app: {
flex: 1
},
item: {
backgroundColor: "#f9c2ff",
padding: 20,
marginVertical: 8,
marginHorizontal: 16
},
title: {
fontSize: 32
}
});
export default App;
I think you should use Reanimated 2 who has a very easy sintaxis and also very powerful. Maybe in combination with RNGestureHandler.
I made a swipeable row inside my FlatList using PanGestureHandler and reanimated 2.
However, my swiping conflicts with the TouchableWithoutFeedback/TouchableOpacity I have when you press on that row.. Is there anyway to disable the onPress while the swiping is happening? I tried doing it like this by using runOnJS(onStart)(), which disables the TouchableOpacity in the parent component, but have been unsuccessful. I think it's because if the onPress was registered before it was disabled, it will still trigger on release.
const Swipeable = props => {
const { onSwipe, children, onStart, onEnd } = props
const translateX = useSharedValue(0)
const scaledButtonX = useSharedValue(scale(50))
const translateXButton = useDerivedValue(() => {
return translateX.value + scaledButtonX.value
})
const offsetX = useSharedValue(0);
const onGestureEvent = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.x = translateX.value
runOnJS(onStart)() // this disables the onPress in the parent component
},
onActive: (event, ctx) => {
translateX.value = ctx.x + clamp(event.translationX, -9999, -ctx.x)
},
onEnd: (event, ctx) => {
const to = snapPoint(translateX.value, event.velocityX, snapPoints)
translateX.value = withTiming(to, {
easing: Easing.linear,
duration: 200
})
ctx.x = translateX.value
runOnJS(onEnd)() // this enables it back
}
})
const style = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
}
})
const buttonStyle = useAnimatedStyle(() => {
return {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
overflow: "hidden",
transform: [{ translateX: translateXButton.value }]
}
})
const onDelete = () => {
translateX.value = withSequence(withTiming(-(width + scale(50))), withTiming(0, {
duration: 500
}, () => console.log('done2')))
// translateX.value = withTiming(0, {}, () => console.log('done'))
onSwipe()
}
return (
<View style={{ position: 'relative' }}>
<Animated.View style={buttonStyle}>
<TouchableOpacity style={styles.deleteButtonContainer} onPress={onDelete}>
<View style={styles.deleteButton}>
<Icon name='x' size={scale(20)} color={Colors.darkPurple} />
</View>
</TouchableOpacity>
</Animated.View>
<PanGestureHandler failOffsetY={[-5, 5]} activeOffsetX={[-5, 5]} onGestureEvent={onGestureEvent}>
<Animated.View style={style}>
{children}
</Animated.View>
</PanGestureHandler>
</View>
)
}
I have created an array of dictionaries that hold the data of audios. The user must record those audios and then they can listen it. the problem is that those audios are represented with a flatlist and when I pass between them, it cannot find the uri. For example:
const [data, setData] = useState(
{uri: null, duration: 0},
{uri: null, duration: 0},
{uri: null, duration: 0}
)
const [index, setIndex] = useState()
const [step, setStep] = useState(0)
const [recording, setRecoding] = useState()
const [sound, setSound] = useState()
const [curDuration, setCurduration] = useState(0)
const [isBuffering, setIsBuferring] = useState(false)
const leftArrow = async () => {
if (index >= 1) {
flatlist.current.scrollToIndex({ animated: true, index: index - 1 })
if (newAudio[index - 1].grabacion != null) {
await sound.unloadAsync()
loadAudio(index - 1)
}
setIndex(index - 1)
};
}
const rightArrow = async () => {
if (index < newAudio.length - 1) {
flatlist.current.scrollToIndex({ animated: true, index: index + 1 })
if (newAudio[index + 1].grabacion != null) {
await sound.unloadAsync()
loadAudio(index + 1)
}
setIndex(index + 1)
}
}
const loadAudio = async (idx) => {
const playbackInstance = new Audio.Sound()
const source = { uri: newAudio[idx].grabacion }
const status = { shouldPlay: isPlaying }
playbackInstance.setOnPlaybackStatusUpdate((status) => { setIsBuffering(status.isBuffering) })
await playbackInstance.loadAsync(source, status, false)
setSound(playbackInstance)
}
const startRecording = async () => {
try {
setStep(1)
console.log('Requesting permissions..');
await Audio.requestPermissionsAsync();
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
playsInSilentModeIOS: true,
playThroughEarpieceAndroid: false,
staysActiveInBackground: true,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DUCK_OTHERS,
shouldDuckAndroid: true,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS,
});
console.log('Starting recording..');
const recording = new Audio.Recording();
await recording.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY);
await recording.startAsync();
setRecording(recording);
console.log('Recording started');
} catch (err) {
console.error('Failed to start recording', err);
}
}
const stopRecording = async () => {
console.log('Stopping recording..');
setRecording(undefined);
await recording.stopAndUnloadAsync();
const uri = recording.getURI();
// set the sound
console.log('Loading Sound');
const { sound } = await Audio.Sound.createAsync({ uri: uri });
setSound(sound);
//check the status
let status = await sound.getStatusAsync()
//save uri and duration
updateValues( status.durationMillis, status.uri);
}
const updateValues = (dur, uri) => {
data[index] = {
duration: dur,
grabacion: uri
}
setNewAudio(newAudio)
}
return (
<View>
<Flatlist
data={data}
horizontal={true}
initialScrollIndex={index}
pagingEnabled={true}
scrollEnabled={false}
initialScrollIndex={index}
showsHorizontalScrollIndicator={false}
keyExtractor={(item, index) => index.toString()}
renderItem={(itemData) => {
<View>
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center'}}>
<TouchableOpacity onPress={leftArrow}>
<AntDesign name='arrowleft' />
</TouchableOpacity>
<TouchableOpacity onPress={rightArrow}>
<AntDesign name='arrowright' />
</TouchableOpacity>
</View>
{step == 0 ?
<TouchableOpacity onPress={startRecording()}>
<Fontisto name='mic' />
</TouchableOpacity >
: step == 1 ?
<TouchableOpacity onPress={stopRecording()}>
<MaterialIcons name='stop' />
</TouchableOpacity >
: <TouchableOpacity onPress={HanddlePlayStop()} >
{isPlaying ?
<Fontisto name='pause' />
:
<Fontisto name='play' />
}
</TouchableOpacity >
}
<Slider
disabled={isPlaying ? true : false}
value={curDuration}
minimumValue={0}
maximumValue={itemData.item.duration}
onSlidingComplete={async (value) => {
await sound.setPositionAsync(value)
}}
/>
</View>
}
/>
</View>
)
The problem that I get is that when I press arrowRight() or arrowLeft() and if the index that we are going to there is already a recording, it does not identify the uri. Can somebody help me with this
I was able to solve it by saving the uri on a UseState and in the updateValues function, I'd change the value grabacion with the useState varable of the uri instead of status.uri. For some reason status.uri was not saving the uri correctly
I have an animation that uses Animated.timing() which slides a component in to view when the condition mapIsCentered = true. When the condition isn't met the component just disappears ungracefully. I'd like for it to slide in and out as the condition changes.
One thing to note the mapIsCentered state is updated on a different screen and passed as a prop to the component I am working in. I have logged the state and it updates when the map is moved.
** the slide in works as expected
Thanks to #Ashwith for the first answer
const values = useRef(new Animated.ValueXY({ x: 0, y: 120 })).current;
useEffect(() => {
Animated.timing(values, {
toValue: mapIsCentered ? { x: 0, y: 0 } : { x: 0, y: 120 },
duration: 500,
useNativeDriver: false,
}).start();
}, [mapIsCentered]);
{!walkInProgress && !hasOnGoingWalks && (
<Animated.View
style={{
transform: [{ translateY: values.y }],
}}
>
<WeatherToast
translations={translations}
loading={loading}
weather={weather}
/>
</Animated.View>
Thanks in advance!
I have changed the structure hope it works for you...
snack: https://snack.expo.io/#ashwith00/excited-orange
App.js
const walkInProgress = false , hasOnGoingWalks = false;
export default function App() {
const { width } = useWindowDimensions();
const [mapCentered, setMapCentered] = React.useState(false)
const toggle = () => {
setMapCentered((ct) => !ct);
};
return (
<View style={styles.container}>
<WeatherToast mapCentered={mapCentered && !walkInProgress && !hasOnGoingWalks} />
<Button title="shift" onPress={toggle} />
</View>
);
}
WeatherTost.js
export default ({ mapCentered }) => {
const [visible, setVisible] = useState(mapCentered);
const { width } = useWindowDimensions();
const values = React.useRef(new Animated.ValueXY({ x: 0, y: 120 })).current;
React.useEffect(() => {
if (mapCentered) {
setVisible(true);
Animated.timing(values, {
toValue: { x: 0, y: 0 },
duration: 300,
}).start();
} else {
Animated.timing(values, {
toValue: { x: width, y: 0 },
duration: 300,
}).start(({ finished }) => {
if (finished) {
setVisible(false);
}
});
}
}, [mapCentered]);
const styles = [];
return visible ? (
<Animated.View
style={{
width: 200,
height: 200,
position: 'absolute',
backgroundColor: 'red',
transform: [
{
translateX: values.x,
},
{
translateY: values.y,
},
],
}}
/>
) : (
<View />
);
};
I have an autoscrolling carousel in React Native and everything is working fine with images scrolling through both automatically every X seconds and manually.
The problem is when I move away from that screen that I get the following error:
Here's is my full code...
const { width, height } = Dimensions.get("window");
let flatList;
function infiniteScroll(dataList) {
const numberOfData = dataList.length;
let scrollValue = 0,
scrolled = 0;
setInterval(function () {
scrolled++;
if (scrolled < numberOfData) scrollValue = scrollValue + width;
else {
scrollValue = 0;
scrolled = 0;
}
this.flatList.scrollToOffset({ animated: true, offset: scrollValue });
}, 3000);
}
const Carousel = (props) => {
const topTenVideos = useSelector(getTopTenVideos);
const dispatch = useDispatch();
const scrollX = new Animated.Value(0);
let position = Animated.divide(scrollX, width);
const [dataList, setDataList] = useState(topTenVideos);
const isFocused = useIsFocused();
useEffect(() => {
if (isFocused) {
setDataList(topTenVideos);
infiniteScroll(dataList);
}
}, [isFocused]);
const renderRow = (itemData) => {
return (
<CarouselItem
id={itemData.item.id}
img={itemData.item.poster}
title={itemData.item.title}
/>
);
};
return (
<View style={styles.screen}>
<FlatList
ref={(flatList) => {
this.flatList = flatList;
}}
horizontal
data={dataList}
pagingEnabled
scrollEnabled
snapToAlignment="center"
scrollEventThrottle={16}
decelerationRate={"fast"}
showsHorizontalScrollIndicator={false}
onScroll={Animated.event([
{ nativeEvent: { contentOffset: { x: scrollX } } },
])}
keyExtractor={(item, index) => "key" + index}
renderItem={renderRow}
/>
<View style={styles.dotView}>
{dataList.map((_, i) => {
let opacity = position.interpolate({
inputRange: [i - 1, i, i + 1],
outputRange: [0.3, 1, 0.3],
extrapolate: "clamp",
});
return (
<Animated.View
key={i}
style={{
opacity,
height: 8,
width: 8,
borderRadius: 6,
backgroundColor: "white",
margin: 8,
}}
/>
);
})}
</View>
</View>
);
};
It's complaining about this line this.flatList.scrollToOffset({ animated: true, offset: scrollValue });
}, 3000); which is inside my infiniteScroll function.
It looks like that when the screen loses focus, it is still searching for this.flatList.scrollToOffset.
You are not creating your ref properly, You have to use useRef hook if you are using functional component or createRef in the case of class component for making refs in your component
Have a look at this.
https://reactjs.org/docs/hooks-reference.html#useref
Create your ref like below.
const flatListRef = useRef(null)
<FlatList
ref={flatListRef}
// other props
/>
flatListRef.current.scrollToOffset({ animated: true, offset: scrollValue }) // access like this.