EDIT: I hate googling for answers and finding some question that never got solved from 10 years ago so I am answering my own question for those that might want to know. In my case, I simply disabled the bounces prop for the scrollview. Since FlatList extends React's ScrollView, setting bounces to false in the animated FlatList component that I created stopped it from bouncing and solved my problem. Have a nice day.
hope you're having a great day. I am trying to animate my header dynamically but for some reason whenever I scroll beyond the beginning or the end of the scrollview, the bounce effect messes with the Animation.(as shown in the gif below)
GIF
Same GIF but higher resolution
As you can see, when I scroll to the top and enable the bounce animation, the header thinks that i am scrolling down as the bounce returns the first element in the list back to the top. How do I fix this? I saw on the web somewhere that adding an interpolator to the animated value would help, though I don't really understand.
Below is my code. Thank You
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
const tempArray = [
...(an array of my data)
]
export default class TempScreen extends React.Component {
static navigationOptions = {
header: null
}
constructor(props) {
super(props)
this.state = {
animatedHeaderValue: new Animated.Value(0),
}
}
render() {
const animatedHeaderHeight = Animated.diffClamp(this.state.animatedHeaderValue, 0, 60)
.interpolate({
inputRange: [0, 70],
outputRange: [70, 0],
})
return ( <
View >
<
Animated.View style = {
{
backgroundColor: 'white',
borderBottomColor: '#DEDEDE',
borderBottomWidth: 1,
padding: 15,
width: Dimensions.get('window').width,
height: animatedHeaderHeight,
}
} >
<
/Animated.View> <
AnimatedFlatList scrollEventThrottle = {
16
}
onScroll = {
Animated.event(
[{
nativeEvent: {
contentOffset: {
y: this.state.animatedHeaderValue
}
}
}]
)
}
data = {
tempArray
}
renderItem = {
({
item
}) =>
<
View style = {
{
flex: 1
}
} >
<
Text style = {
{
fontWeight: 'bold',
fontSize: 30
}
} > {
item.name
} < /Text> <
Text > {
item.year
} < /Text> <
/View>
}
/>
<
/View>
)
}
}
If you want to solve the "bounce" problem only, the problem is that iOS gives to diffClamp negative scrollY values. You need to filter these and ensure scrollY remains >= 0 to avoid diffClamp being affected by overscroll.
const clampedScrollY = scrollY.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
Another nice trick is to use a "cliff" technique, so that the header only disappear after a minimum scrollY position.
Here is code from my app:
const minScroll = 100;
const clampedScrollY = scrollY.interpolate({
inputRange: [minScroll, minScroll + 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
const minusScrollY = Animated.multiply(clampedScrollY, -1);
const translateY = Animated.diffClamp(
minusScrollY,
-AnimatedHeaderHeight,
0,
);
const opacity = translateY.interpolate({
inputRange: [-AnimatedHeaderHeight, 0],
outputRange: [0.4, 1],
extrapolate: 'clamp',
});
clampedScrollY will be:
0 when scrollY=0
0 when scrollY=50
0 when scrollY=100
30 when scrollY=130
170 when scrollY=270
You get the idea. So diffClamp will only be > 0 if scrollY > 100, and increment 1 by 1 after that threshold.
I had the same problem like two hour ago...
You can set Scrollview property bounces=false but if you want a RefreshControl for refreshing the ScrollView content (like in my case), the bounce property has to stay active.
I fixed this following this cool article: https://medium.com/appandflow/react-native-collapsible-navbar-e51a049b560a.
I'm not an expert of the Animated library, so I post my code:
constructor(props) {
const scrollAnim = new Animated.Value(0);
const offsetAnim = new Animated.Value(0);
this.state = {
scrollAnim,
offsetAnim,
AnimatedViewHeight: 1,
clampedScroll: Animated.diffClamp(
Animated.add(
scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
offsetAnim
),0, 1
)
}
}
render() {
const minScroll = this.state.AnimatedViewHeight;
const navbarTranslate = this.state.clampedScroll.interpolate({
inputRange: [0, minScroll],
outputRange: [0, -minScroll],
extrapolate: 'clamp',
});
return (
<View style={{
flex: 1
}}>
<Animated.View
onLayout={(event) => {
var { height } = event.nativeEvent.layout;
this.setState({
AnimatedViewHeight: height,
clampedScroll: Animated.diffClamp(
Animated.add(
this.state.scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
this.state.offsetAnim
), 0, height)
})
}}
style={[{ transform: [{ translateY: navbarTranslate }] }]}>
<View><Text>THIS IS YOUR HEADER</Text></View>
</Animated.View>
<AnimatedFlatList
// iOS offset for RefreshControl
contentInset={{
top: this.state.AnimatedViewHeight,
}}
contentOffset={{
y: -this.state.AnimatedViewHeight,
}}
scrollEventThrottle={1}
onScroll={
Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scrollAnim } } }],
{ useNativeDriver: true },
)}
data={this.state.data}
keyExtractor={(item, idx) => idx}
ListFooterComponent={this.renderFooter}
renderItem={this.renderItem}
onEndReached={this.handleLoadMore}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
// Android offset for RefreshControl
progressViewOffset={this.state.AnimatedViewHeight}
/>
}
onEndReachedThreshold={0.5} />
</View>
)
}
this.state.AnimatedViewHeight is the height of the header, retrieved by calling onLayout function. Inside of this function I also set a new clampedScroll because I have a new height (In my case, the header doesn't have a fixed size).
After that, in render(), define a variable (navbarTranslate) for control the headerSize based on the scroll position of your Animated Scrollview.
This is an implementation that works for bounces in both directions (at the start and at the end of the list).
const minScroll = 100;
const headerHeight = 65;
const activeRange = 200;
const yOffset = useRef(new Animated.Value(0)).current;
const diffClamp = Animated.diffClamp(
yOffset,
-minScroll,
activeRange + minScroll
);
const translateY = diffClamp.interpolate({
inputRange: [0, activeRange],
outputRange: [0, -headerHeight],
extrapolate: "clamp",
});
The yOffset is passed to the onScroll prop of the ScrollView or Flatlist, e.g.,
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: yOffset,
},
},
},
],
{ useNativeDriver: false }
)}
scrollEventThrottle={10}
bounces={true}
It works as follows:
yOffset is the scrolled distance within the list and can take any value depending on the length of the list.
diffClamp maps the yOffset to a value between -minScroll and activeRange + minScroll. When yOffset is outside of this range, diffclamp essentially shifts this range to the difference outside the range so that it is always active when you change the scroll direction.
translateY is the value with which you want the header to move. The interpolation of translateY only accepts inputs in the range: 0 to activeRange, and thus does not respond to unless the scrolled distance is larger than minScroll (in both directions). The output equals -headerHeight to move the header up, and this output is clamped to keep the header hidden as the scrolling continues.
Note that by changing the activeRange, you can change the speed at which the header moves.
I resolved using this answer https://stackoverflow.com/a/51638296/3639398
import React from 'react';
import {
Animated,
Text,
View,
StyleSheet,
ScrollView,
Dimensions,
RefreshControl,
} from 'react-native';
import Constants from 'expo-constants';
import randomColor from 'randomcolor';
const HEADER_HEIGHT = 44 + Constants.statusBarHeight;
const BOX_SIZE = Dimensions.get('window').width / 2 - 12;
const wait = (timeout: number) => {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
};
function App() {
const [refreshing, setRefreshing] = React.useState(false);
const scrollAnim = new Animated.Value(0);
const minScroll = 100;
const clampedScrollY = scrollAnim.interpolate({
inputRange: [minScroll, minScroll + 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
const minusScrollY = Animated.multiply(clampedScrollY, -1);
const translateY = Animated.diffClamp(minusScrollY, -HEADER_HEIGHT, 0);
const onRefresh = React.useCallback(() => {
setRefreshing(true);
wait(2000).then(() => {
setRefreshing(false);
});
}, []);
return (
<View style={styles.container}>
<Animated.ScrollView
contentContainerStyle={styles.gallery}
scrollEventThrottle={1}
bounces={true}
showsVerticalScrollIndicator={false}
style={{
zIndex: 0,
height: '100%',
elevation: -1,
}}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollAnim } } }],
{ useNativeDriver: true }
)}
overScrollMode="never"
contentInset={{ top: HEADER_HEIGHT }}
contentOffset={{ y: -HEADER_HEIGHT }}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
{Array.from({ length: 20 }, (_, i) => i).map((uri) => (
<View style={[styles.box, { backgroundColor: 'grey' }]} />
))}
</Animated.ScrollView>
<Animated.View style={[styles.header, { transform: [{ translateY }] }]}>
<Text style={styles.title}>Header</Text>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
gallery: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: 4,
},
box: {
height: BOX_SIZE,
width: BOX_SIZE,
margin: 4,
},
header: {
flex: 1,
height: HEADER_HEIGHT,
paddingTop: Constants.statusBarHeight,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: randomColor(),
},
title: {
fontSize: 16,
},
});
export default App;
checkout on Expo https://snack.expo.io/#raksa/auto-hiding-header
Related
I am trying to get a good understanding of how to use MotionLayout in Jetpack Compose.
At this state, I believe to have a basic understanding of how the MotionLayout works, by having a MotionScene (defined in a .json5 file) set to the MotionLayout and then apply a Modifier.layoutId to all the Composables, which should have an effect in the animation.
What I am trying to do, is to have a circle, which should stretch out on the X-axis, left side first (for maybe 2-300 ms) then have the right side follow along awards the left (for 2-300 ms), so that it will be a full circle once again - Just at a different position.
An example of what I wish to have, is an animation similar to the three images here.
Sadly I haven't been able to do that yet. In the Transitions part of the MotionScene, I have played around with scaleX, which made the circle misshaped and stretched out to both sides at the same time. And translationX which ofc, just moves the entire circle instead of just a part of it.
My current implementation looks like this:
Composable
#OptIn(ExperimentalMotionApi::class)
#Preview
#Composable
fun AnimationScreen() {
val context = LocalContext.current
val motionScene = remember {
context.resources.openRawResource(R.raw.motion_scene).readBytes().decodeToString()
}
var progress by remember { mutableStateOf(0f) }
Column {
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = progress,
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.layoutId("row_container")
) { }
Box(
modifier = Modifier
.size(100.dp)
.clip(
RoundedCornerShape(
topEnd = 25.dp,
bottomEnd = 25.dp,
topStart = 25.dp,
bottomStart = 25.dp
)
)
.background(Color.Red)
.layoutId("circle")
)
}
Spacer(Modifier.height(32.dp))
Slider(
value = progress,
onValueChange = {
progress = it
}
)
}
}
motion_scene.json5
{
ConstraintSets: {
start: {
circle: {
width: 40,
height: 40,
start: ['logo_pic', 'start', 0],
end: ['logo_pic', 'end', 0],
top: ['logo_pic', 'top', 0],
bottom: ['logo_pic', 'bottom', 0]
},
},
end: {
circle: {
width: 40,
height: 40,
start: ['logo_pic', 'start', 0],
end: ['logo_pic', 'end', 0],
top: ['logo_pic', 'top', 0],
bottom: ['logo_pic', 'bottom', 0],
},
},
},
Transitions: {
default: {
from: 'start',
to: 'end',
pathMotionArc: 'startHorizontal',
KeyFrames: {
KeyAttributes: [
{
target: ['circle'],
frames: [0, 50, 80, 100],
scaleX: [1, 1, 2, 2],
//translationX: [0, 0, 0, -150]
},
],
KeyPosition: [
{
target: ['circle'],
percentWidth: 20
}
]
}
}
}
}
Hopefully it all comes down to my just being new to this framework, and someone just says, "Easy! You just need to...".
Any suggestions on, how I would make this work?
There is not a simple way to do this with motionlayout
Because the start and end are the same size.
But you can add a middle and play some trick with progress.
It was coded this way to avoid a few bugs in the latest release.
public const val motionSceneStr = """
{
ConstraintSets: {
start: {
circle: {
width: 40,
height: 40,
start: ['parent', 'start', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0]
},
},
middle: {
circle: {
width: 'spread',
height: 40,
start: ['parent', 'start', 0],
end: ['parent', 'end', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0],
},
},
end: {
circle: {
width: 40,
height: 40,
end: ['parent', 'end', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0],
},
},
},
Transitions: {
part2: { from: 'middle', to: 'end' }
part1: { from: 'middle', to: 'start' }
}
}
""";
#OptIn(ExperimentalMotionApi::class)
#Preview
#Composable
fun AnimationScreen() {
val context = LocalContext.current
val motionScene = remember {motionSceneStr }
var progress by remember { mutableStateOf(0f) }
Column {
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = if (progress<0.5) 1-progress*2 else progress*2-1,
transitionName = if (progress<0.5f) "part1" else "part2",
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.layoutId("row_container")
) { }
Box(
modifier = Modifier
.size(100.dp)
.clip(
RoundedCornerShape(
topEnd = 25.dp,
bottomEnd = 25.dp,
topStart = 25.dp,
bottomStart = 25.dp
)
)
.background(Color.Red)
.layoutId("circle")
)
}
Spacer(Modifier.height(32.dp))
Slider(
value = progress,
onValueChange = {
progress = it
}
)
}
}
Apply gradient colours to the background of the active dots of paginator in react native.
I tried with most of the npm packages but most of them available for native only.
I want generic for both ios and android.Given my code as well.
export default Paginator = ({ data, scrollX, index }) => {
const { width } = useWindowDimensions();
return (
<View style={styles.paginatorView}>
{data.map((_, i) => {
const inputRange = [(i - 1) * width, i * width, (i + 1) * width];
const dotwidth = scrollX.interpolate({
inputRange,
outputRange: [10, 40, 10],
extrapolate: "clamp",
});
const opacity = scrollX.interpolate({
inputRange,
outputRange: [0.4, 1, 0.4],
extrapolate: "clamp",
});
return (
<Animated.View
style={[
styles.dot,
{
width: dotwidth,
opacity: opacity,
},
i === index && styles.dotActive,
]}
key={i.toString()}
/>
);
})}
</View>
);
};
const styles = StyleSheet.create({
dot: {
height: moderateScale(10),
borderRadius: moderateScale(5),
backgroundColor: COLORS.grey,
marginHorizontal: moderateScale(5),
},
dotActive: {
backgroundColor: COLORS.blue,
},
paginatorView: {
flexDirection: "row",
height: moderateScale(65),
justifyContent: "center",
},
});
Currently I had used static of Blue color,I want to make it gradient of two colors.
I have also made a linear button using this library.
it works fine, you need to take care of its setup.
You can use this library
https://www.npmjs.com/package/react-native-linear-gradient
==> You can use it like this.
return (
<LinearGradient colors={backgroundColor}>
<Animated.View
style={[
styles.dot,
{
width: dotwidth,
opacity: opacity,
},
i === index && styles.dotActive,
]}
key={i.toString()}
/>
</LinearGradient>
);
I created a responsive rect as an artboard in the stage, and when I add shapes and move them, I want the shapes to only show on the rect, just like opening a window on the stage, only show the shapes inside the window
You should use clipping for that. Tutorial: https://konvajs.org/docs/clipping/Clipping_Regions.html
import React from "react";
import { createRoot } from "react-dom/client";
import { Stage, Layer, Star } from "react-konva";
function generateShapes() {
return [...Array(50)].map((_, i) => ({
id: i.toString(),
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
rotation: Math.random() * 180,
isDragging: false
}));
}
const INITIAL_STATE = generateShapes();
const STAR_PROPS = {
numPoints: 5,
innerRadius: 20,
outerRadius: 40,
fill: "#89b717",
opacity: 0.8,
shadowColorL: "black",
shadowBlur: 10,
shadowOpacity: 0.6,
shadowOffsetX: 5,
shadowOffsetY: 5,
scaleX: 1,
scaleY: 1,
draggable: true
};
const App = () => {
const [stars] = React.useState(INITIAL_STATE);
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer
clipX={50}
clipY={50}
clipWidth={window.innerWidth - 100}
clipHeight={window.innerWidth - 100}
>
{stars.map((star) => (
<Star
{...STAR_PROPS}
key={star.id}
id={star.id}
x={star.x}
y={star.y}
rotation={star.rotation}
/>
))}
</Layer>
</Stage>
);
};
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);
https://codesandbox.io/s/react-konva-clipping-sg2fpd
I have an animation for a bottom bar that goes under the tab text, the animation is working fine in android but not in ios. I am using react-native-reanimated.
Any help would be appreciated.
Thanks
const MyTabBar = React.memo((props) => {
const {state, descriptors, navigation, position, setEnableSwipe, swipeEnabled, layout, theme, auth, ui} = props;
if (state.routes[state.index].state && state.routes[state.index].state.index !== 0) {
if (swipeEnabled === true) {
setEnableSwipe(false)
}
return null;
}
else {
if (swipeEnabled === false) {
setEnableSwipe(true);
}
var tabWidth = (layout.width - 50)/3
const left = Animated.interpolate(position, {
inputRange: [0, 1, 2],
outputRange: [(tabWidth - 50)/2 , tabWidth + (tabWidth - 50)/2, 2*tabWidth + (tabWidth - 50)/2]
});
const length = Animated.interpolate(position, {
inputRange: [0, 0.5, 1, 1.5, 2],
outputRange: [0.3, 1, 0.3, 1, 0.3],
})
return (
<View style={{ flexDirection: 'row', backgroundColor: Platform.OS === 'ios' && ui.showing_modal ? 'white' : 'white', alignItems: 'center' }}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
const inputRange = state.routes.map((_, i) => i);
const opacity = Animated.interpolate(position, {
inputRange,
outputRange: inputRange.map(i => (i === index ? 1 : 0.4)),
});
return (
<TouchableOpacity
accessibilityRole="button"
accessibilityStates={isFocused ? ['selected'] : []}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
onLongPress={onLongPress}
style={{flex: 1}}
>
<Animated.Text style={{
opacity,
fontWeight: index === state.index ? '600' : 'normal',
fontSize: index === state.index ? 19 : 17,
textAlign: 'center',
}}>
{label}
</Animated.Text>
</TouchableOpacity>
);
})}
{Platform.OS === 'ios' && false ?
<View/> : <Animated.View
style={{
backgroundColor: theme.primaryColor,
translateX: left,
scaleX: length,
height: 4,
width: 50,
position: 'absolute',
bottom: 0,
borderRadius: 10,
}} />}
<TouchableOpacity
style={{minWidth: 50, maxWidth: 50}}
onPress={() => {
switch (state.index) {
case 0:
navigation.navigate('AddSchedule');
break;
case 1:
navigation.navigate('AddScene');
break;
case 2:
if (auth.accesstoken) {
navigation.navigate('NewGeoscene');
} else {
ReactNativeHapticFeedback.trigger('notificationWarning', {
ignoreAndroidSystemSettings: true,
enableVibrateFallback: true
})
}
break;
default:
//
}
}}
>
<Text style={{fontSize: 36, color: theme.primaryColor}}> + </Text>
</TouchableOpacity>
</View>
);
}
})
this is my code, the line Animated.View doesn't animate in iOS so I am not rendering it there, but i want it working.
Android
Expected behaviour (android)
iOS:
behaviour in iOS
I have an observation from my own code which may or may not be applicable here. This observation is only true if both of the transform properties are using react-native-reanimated animation values. Your code appears to match this scenario.
In iOS only, I must place the scale property before the translateX property in my transform object. If I put the translateX property before the scale property then the translateX property is impeded.
I have no explanation for this, but I have tested it with scaleX since that's what you are using and the same is true.
So for clarity, the following works:
<Animated.Value
style={[{
transform: [{
scale: animationValue1,
translateX: animationValue2,
}]
}]}
/>
..while in the next, the scale works but the translateX does not:
<Animated.Value
style={[{
transform: [{
translateX: animationValue2,
scale: animationValue1,
}]
}]}
/>
You'll notice my syntax is a little different from yours too. As far as I'm aware you shouldn't be putting the translateX and scaleX properties in the style object without being wrapped in a transform property, as above. Perhaps reanimated's View works a bit differently to react native's native one.. If it's still not working after you try reordering the properties then give this some thought too.
See the documentation here: https://reactnative.dev/docs/transforms#transform
It's a bit confused because the API describes transform as a function, but the example at the top of the page shows it used as I have done above.
I want to add tabs at the bottom of my home screen but I don't manage how to do it.
I want two tabs like "Map" (for my homepage) and "Settings". I don't know how to create that, I tried to add some codes inside but it's not working. Do you have ideas of what I'm doing wrong?
Do I need to add these codes inside ?
class HomeScreen extends React.Component {
render() {
return (
class SettingsScreen extends React.Component {
render() {
return (
I also tried to add this code at the bottom:
const TabNavigator = createBottomTabNavigator({
MAP: { screen: HomeScreen },
SETTINGS: { screen: SettingsScreen },
});
export default createAppContainer(TabNavigator);
Here is my code:
import React, { Component } from 'react';
import { StyleSheet, Text, View, Animated, Image, Dimensions } from "react-native";
import { Components, MapView } from 'expo';
const Images = [
{ uri: "https://images.unsplash.com/photo-1555706655-6dd427c11735?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80" },
{ uri: "https://images.unsplash.com/photo-1555706741-8f39aa887cf7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80" },
{ uri: "https://images.unsplash.com/photo-1555706741-fade7dd756a9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80" },
{ uri: "https://images.unsplash.com/photo-1555706742-67a1170e528d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80" }
]
const { width, height } = Dimensions.get("window");
const CARD_HEIGHT = height / 5;
const CARD_WIDTH = CARD_HEIGHT - 50;
export default class screens extends Component {
state = {
markers: [
{
coordinate: {
latitude: 41.414494,
longitude: 2.152695,
},
title: "Parc Güell",
description: "One of the best view in Barcelona. ",
image: Images[0],
},
{
coordinate: {
latitude: 41.403706,
longitude: 2.173504,
},
title: "Sagrada Familia",
description: "This is the second best place in Portland",
image: Images[1],
},
{
coordinate: {
latitude: 41.395382,
longitude: 2.161961,
},
title: "Casa Milà",
description: "This is the third best place in Portland",
image: Images[2],
},
{
coordinate: {
latitude: 41.381905,
longitude: 2.178185,
},
title: "Gothic Quarter",
description: "This is the fourth best place in Portland",
image: Images[3],
},
],
region: {
latitude: 41.390200,
longitude: 2.154007,
latitudeDelta: 0.04864195044303443,
longitudeDelta: 0.040142817690068,
},
};
componentWillMount() {
this.index = 0;
this.animation = new Animated.Value(0);
}
componentDidMount() {
// We should detect when scrolling has stopped then animate
// We should just debounce the event listener here
this.animation.addListener(({ value }) => {
let index = Math.floor(value / CARD_WIDTH + 0.3); // animate 30% away from landing on the next item
if (index >= this.state.markers.length) {
index = this.state.markers.length - 1;
}
if (index <= 0) {
index = 0;
}
clearTimeout(this.regionTimeout);
this.regionTimeout = setTimeout(() => {
if (this.index !== index) {
this.index = index;
const { coordinate } = this.state.markers[index];
this.map.animateToRegion(
{
...coordinate,
latitudeDelta: this.state.region.latitudeDelta,
longitudeDelta: this.state.region.longitudeDelta,
},
350
);
}
}, 10);
});
}
render() {
const interpolations = this.state.markers.map((marker, index) => {
const inputRange = [
(index - 1) * CARD_WIDTH,
index * CARD_WIDTH,
((index + 1) * CARD_WIDTH),
];
const scale = this.animation.interpolate({
inputRange,
outputRange: [1, 2.5, 1],
extrapolate: "clamp",
});
const opacity = this.animation.interpolate({
inputRange,
outputRange: [0.35, 1, 0.35],
extrapolate: "clamp",
});
return { scale, opacity };
});
return (
<View style={styles.container}>
<MapView
ref={map => this.map = map}
initialRegion={this.state.region}
style={styles.container}
>
{this.state.markers.map((marker, index) => {
const scaleStyle = {
transform: [
{
scale: interpolations[index].scale,
},
],
};
const opacityStyle = {
opacity: interpolations[index].opacity,
};
return (
<MapView.Marker key={index} coordinate={marker.coordinate}>
<Animated.View style={[styles.markerWrap, opacityStyle]}>
<Animated.View style={[styles.ring, scaleStyle]} />
<View style={styles.marker} />
</Animated.View>
</MapView.Marker>
);
})}
</MapView>
<Animated.ScrollView
horizontal
scrollEventThrottle={1}
showsHorizontalScrollIndicator={false}
snapToInterval={CARD_WIDTH}
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
x: this.animation,
},
},
},
],
{ useNativeDriver: true }
)}
style={styles.scrollView}
contentContainerStyle={styles.endPadding}
>
{this.state.markers.map((marker, index) => (
<View style={styles.card} key={index}>
<Image
source={marker.image}
style={styles.cardImage}
resizeMode="cover"
/>
<View style={styles.textContent}>
<Text numberOfLines={1} style={styles.cardtitle}>{marker.title}</Text>
<Text numberOfLines={1} style={styles.cardDescription}>
{marker.description}
</Text>
</View>
</View>
))}
</Animated.ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
position: "absolute",
bottom: 30,
left: 0,
right: 0,
paddingVertical: 10,
},
endPadding: {
paddingRight: width - CARD_WIDTH,
},
card: {
padding: 10,
elevation: 2,
backgroundColor: "#FFF",
marginHorizontal: 10,
shadowColor: "#000",
shadowRadius: 5,
shadowOpacity: 0.3,
shadowOffset: { x: 2, y: -2 },
height: CARD_HEIGHT,
width: CARD_WIDTH,
overflow: "hidden",
},
cardImage: {
flex: 3,
width: "100%",
height: "100%",
alignSelf: "center",
},
textContent: {
flex: 1,
},
cardtitle: {
fontSize: 12,
marginTop: 5,
fontWeight: "bold",
},
cardDescription: {
fontSize: 12,
color: "#444",
},
markerWrap: {
alignItems: "center",
justifyContent: "center",
},
marker: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: "rgba(130,4,150, 0.9)",
},
ring: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: "rgba(130,4,150, 0.3)",
position: "absolute",
borderWidth: 1,
borderColor: "rgba(130,4,150, 0.5)",
},
});
create a new file for tab navigation and add the following code:
(I imported your map component as screens)
import React from 'react';
import { Platform } from 'react-native';
import { createStackNavigator, createBottomTabNavigator } from 'react-navigation';
import TabBarIcon from '../components/TabBarIcon';
import screens from '../screens/HomeScreen';
import SettingsScreen from '../screens/SettingsScreen';
const HomeStack = createStackNavigator({
Home: screens,
});
HomeStack.navigationOptions = {
tabBarLabel: 'Home',
tabBarIcon: ({ focused }) => (
<TabBarIcon
focused={focused}
name={
Platform.OS === 'ios'
? `ios-information-circle${focused ? '' : '-outline'}`
: 'md-information-circle'
}
/>
),
};
const SettingsStack = createStackNavigator({
Settings: SettingsScreen,
});
SettingsStack.navigationOptions = {
tabBarLabel: 'Settings',
tabBarIcon: ({ focused }) => (
<TabBarIcon
focused={focused}
name={Platform.OS === 'ios' ? 'ios-options' : 'md-options'}
/>
),
};
export default createBottomTabNavigator({
HomeStack,
SettingsStack,
});