hide header base on scroll delta not scroll position - react-native

how I can get scroll change from start to end with react native flat list and change the height of header base on that like bellow example
example-screenshot
for instance
const delta = end_scroll_pos - start_scroll_pos; // for example delta = 60
for example between scroll postion 220 and 280 delta 60

I've recently developed a new package, react-native-animated-screen, that does exactly what you need
Check it out
https://www.npmjs.com/package/react-native-animated-screen
import React from 'react';
import { Image, Text, View } from 'react-native';
import AnimatedScreen from 'react-native-animated-screen';
const Component = () => {
const animationStart = 90;
const animationEnd = 300; // the header will be totally collapsed by screen scrolled 300
return (
<AnimatedScreen.Wrapper>
<AnimatedScreen.Header headerMinHeight={animationStart} headerMaxHeight={animationEnd}>
<View>
<Text>Title</Text>
<AnimatedScreen.CollapsibleElement>
<Text>Subtitle</Text>
</AnimatedScreen.CollapsibleElement>
</View>
</AnimatedScreen.Header>
<AnimatedScreen.ScrollView>
<View style={{ height: '300%' }}>
<View>
<Text>Body</Text>
</View>
</View>
</AnimatedScreen.ScrollView>
</AnimatedScreen.Wrapper>
);
};
In the example above only the title is going to remain after scrolling, the subtitle (as all the elements wrapped in a CollapsibleElement) will disappear.
You can use the two params headerMinHeight and headerMaxHeight of the AnimatedHeader wrapper component to define exactly after how much scroll the animation should start and complete

Related

Reanimated: animated view doesn't react to value changes

What you will see below is a minimised version of a bigger draggable solution I'm trying to implement—and it requires to have an animated view that would react to changes in animated style. This example isn't containing any gesture code since it's irrelevant here.
I have two rectangles: first is a Button that changes offset value randomly; second one is AnimatedRectangle that is supposed to be changing position each time the Button is pressed. That's it.
Expected result: AnimatedRectangle moving when Button is pressed.
Actual result: nothing moves.
FYI: share values, as well as animated style are changing, but the animated view doesn't seem to react to these changes.
Weird part is that when I was trying the same code in another project it worked in some files but not in others, although the styling and the way these different components were defined are the same. I have no idea why it happens.
Steps to reproduce:
Click on the Button
Observe the blue rectangle
Repo link: https://github.com/tumanov-alex/reanimated-not-working
import React from 'react';
import {View, TouchableOpacity, Animated, Text} from 'react-native';
import {useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
const App = () => {
const offset = useSharedValue({x: 0, y: 0});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{translateX: offset.value.x}, {translateY: offset.value.y}],
}));
const Button = () => (
<TouchableOpacity
onPress={() => {
offset.value = {
x: Math.random() * 100,
y: Math.random() * 100,
};
}}>
<View style={{width: 500, height: 500, backgroundColor: 'grey'}} />
</TouchableOpacity>
);
const AnimatedRectangle = () => (
<Animated.View
style={[
animatedStyle,
{
width: 100,
height: 100,
backgroundColor: 'blue',
},
]}>
<Text style={{color: 'white'}}>Why I'm not moving?</Text>
</Animated.View>
);
return (
<View style={{flex: 1}}>
<Button />
<AnimatedRectangle />
</View>
);
};
export default App;
Reanimated version: 2.11.0
React Native version: 0.70.3
Platforms: Android, iOS
Device: iOS simulator
I think that you have to import Animated directly from react-native-reanimated library and not from react-native:
Like this.
import Animated, {useAnimatedStyle, useSharedValue} from 'react-native-reanimated';

Increase height of a View on swipe up in react native expo

I have a container that contains multiple views like this :
export default function MyComponent() {
<View *** container *** >
<View> // some stuff </View>
<View> // some stuff </View>
<ScrollView> // some stuff </ScrollView>
</View
}
The ScrollView is about 40% of the container's height, in absolute position.
What I need to do is to be able to extend it over the whole screen with a swipe up.
I tried to use some modals npm package but I can't make it work.
A few things:
From my experience, ScrollViews and FlatLists work best when they have a flex of one and are wrapped in a parent container that limits their size.
I couldnt determine if you wanted to wrap the entire screen in a GestureDector and listen to swipes or if you only wanted the ScrollView to listen for scroll events. Because you want the ScrollView to take up the entire screen, I assume you wanted to listen to onScroll events
So here's a demo I put together:
import * as React from 'react';
import {
Text,
View,
Animated,
StyleSheet,
ScrollView,
useWindowDimensions
} from 'react-native';
import Constants from 'expo-constants';
import Box from './components/Box';
import randomColors from './components/colors'
const throttleTime = 200;
// min time between scroll events (in milliseconds)
const scrollEventThrottle = 100;
// min up/down scroll distance to trigger animatino
const scrollYThrottle = 2;
export default function App() {
const scrollViewAnim = React.useRef(new Animated.Value(0)).current;
let lastY = React.useRef(0).current;
// used to throttle scroll events
let lastScrollEvent = React.useRef(Date.now()).current;
const [{ width, height }, setViewDimensions] = React.useState({});
const [isScrollingDown, setIsScrollingDown] = React.useState(false);
const [scrollViewTop, setScrollViewTop] = React.useState(400);
// scroll view is 40% of view height
const defaultHeight = height * .4;
// call onLayout on View before scrollView
const onLastViewLayout = ({nativeEvent})=>{
// combine the y position with the layout height to
// determine where to place scroll view
setScrollViewTop(nativeEvent.layout.y + nativeEvent.layout.height)
}
const onContainerLayout = ({nativeEvent})=>{
// get width and height of parent container
// using this instead of useWindowDimensions allow
// makes the scrollView scale with parentContainer size
setViewDimensions({
width:nativeEvent.layout.width,
height:nativeEvent.layout.height
})
}
//animation style
let animatedStyle = [styles.scrollView,{
height:scrollViewAnim.interpolate({
inputRange:[0,1],
outputRange:[defaultHeight,height]
}),
width:width,
top:scrollViewAnim.interpolate({
inputRange:[0,1],
outputRange:[scrollViewTop,-10]
}),
bottom:60,
left:0,
right:0
}]
const expandScrollView = ()=>{
Animated.timing(scrollViewAnim,{
toValue:1,
duration:200,
useNativeDriver:false
}).start()
}
const shrinkScrollView = ()=>{
Animated.timing(scrollViewAnim,{
toValue:0,
duration:200,
useNativeDriver:false
}).start()
}
const onScroll=(e)=>{
// throttling by time between scroll activations
if(Date.now() - lastScrollEvent <scrollEventThrottle ){
console.log('throttling!')
return
}
lastScrollEvent = Date.now()
// destructure event object
const {nativeEvent:{contentOffset:{x,y}}} = e;
const isAtTop = y <= 0
const isPullingTop = lastY <= 0 && y <= 0
let yDiff = y - lastY
let hasMajorDiff = Math.abs(yDiff) > scrollYThrottle
// throttle if isnt pulling top and scroll dist is small
if(!hasMajorDiff && !isPullingTop ){
return
}
const hasScrolledDown = yDiff > 0
const hasScrolledUp = yDiff < 0
if(hasScrolledDown){
setIsScrollingDown(true);
expandScrollView()
}
if(isAtTop || isPullingTop){
setIsScrollingDown(false)
shrinkScrollView();
}
lastY = y
}
return (
<View style={styles.container} onLayout={onContainerLayout}>
<Box color={randomColors[0]} text="Some text"/>
<Box color={ randomColors[1]} text="Some other text "/>
<View style={styles.lastView}
onLayout={onLastViewLayout}>
<Text>ScrollView Below </Text>
</View>
<Animated.View style={animatedStyle}>
<ScrollView
onScroll={onScroll}
style={{flex:1}}
>
{randomColors.map((color,i)=>
<Box color={color} height={60} text={"Item Number "+(i+1)}/>
)}
</ScrollView>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
// justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
padding: 8,
},
scrollView:{
// position:'absolute',
position:'absolute',
marginVertical:10,
height:'40%',
backgroundColor:'lightgray'
},
lastView:{
alignItems:'center',
paddingVertical:5,
borderBottomWidth:1,
borderTopWidth:1
}
});
The result is that on downward scrolling, the scrollview expands and takes up the entire screen, and shrinks when the user scrolls to the top.
Edit : I found that simply grabbing the y position and the height of the view directly before the scroll view made it easy to calculate where the position the ScrollView, allowing for the ScrollView to be positioned absolute all the time.
Here is a very basic example of how to use FlatList (similar to ScrollView) and allow for the scrolling behavior you are wanting:
import React from "react";
import {Text,View} from "react-native";
const App = () => {
const myData = {//everything you want rendered in flatlist}
const renderSomeStuff = () => {
return (
<View>
<Text> Some Stuff </Text>
</View>
)
};
const renderOtherStuff = () => {
return (
<View>
<Text> Other Stuff </Text>
</View>
);
};
return (
<View>
<FlatList
data={myData}
keyExtractor={(item) => `${item.id}`}
showsVerticalScrollIndicator
ListHeaderComponent={
<View>
{renderSomeStuff()}
{renderOtherStuff()}
</View>
}
renderItem={({ item }) => (
<View>
<Text>{item}</Text>
</View>
)}
ListFooterComponent={
<View></View>
}
/>
</View>
);
};
export default App;

How would you do that opening effect with React Native?

Imagine a feed of items and then you click on one item and it opens in this way. We need to create a news app that has this identical animation (check sec 00:13 of video linked). enter link description here
You can use the react-native-collapsible package to achieve something like this
https://github.com/oblador/react-native-collapsible
One way to do this is to do dynamic styling.
like this:
import React from "react";
import { View, TouchableOpacity} from "react-native";
const Component = () => {
const [isOpen, setisOpen] = React.useState(false);
return (
<TouchableOpacity onPress={() => setisOpen(!isOpen)}>
<View>
{/* The View content that is always open */}
</View>
<View style={isOpen ? { disply: "flex" } : { disply: "none" }}>
{/* The View content to be opened/hidden */}
</View>
</TouchableOpacity>
);
};
then you can just import this component in your screen like this:
<Component />
Depend on the navigation library you are using but I will assume that you are using react-navigation.
You have this package to perform an animation with shared element during navigation between two screens: https://github.com/IjzerenHein/react-navigation-shared-element.
Very easy to use and very performant.

Applying styles in react native with styled components

I have a component, Logo, where I'm trying to apply a width and a height:
import styledNative from "styled-components/native";
import {Image, View} from "react-native"
import companyIcon from "../svg/companyIcon.svg";
const Logo = () => (
<Image source={{uri: companyIcon}} />;
);
const StyledLogo = styledNative(Logo)`
width: 48px;
height: 48px;
`;
const Header = () => {
return (
<View>
<StyledLogo />
</View>
);
};
This leaves us with an invisible logo, with a width of 0. Styled-components docs seems to suggest using a passed in className property (https://www.styled-components.com/docs/advanced#existing-css), however this seems to be for standard styled-components, not react-native / react-native-web implementation, because it's just not passed to my component. When I examine the passed props for Logo (by console logging them), there is a style object, but this doesn't appear to be anything I can use and there's not much mention of it in the styled component documentation:
style: Array(2)
0: 91
1: undefined
length: 2
So I'm not really sure what to do with it. How can components be styled by styled-components?
You aren't applying your styles to the image.
What is happening now, is that your Logo component is receiving those style-props, but it's not passing them through.
const Logo = (props) => ( // <-- containing style props
<Image source={{uri: companyIcon}} />;
);
So what you would want to do is to spread your Logos props over your Image, so it gets the data it needs.
const Logo = (props) => ( // <-- containing style props
<Image {...props} source={{uri: companyIcon}} />; // <-- styles now applied to Image
);
Or alternatively you can pass through just the style-props.
const Logo = (props) => ( // <-- containing style props
<Image source={{uri: companyIcon}} style={props.style}/>;
);
Edit:
It also appears you are passing invalid styles. React Native expects the values you pass in to not have units. As such, your styles should be width: 48; instead, dropping the ...px.

React Native <ScrollView> persistent scrollbar

After perusing the React Native Documentation I couldn't seem to find out how to make a <ScrollView> have a persistent scrollbar that doesn't fade out. How would I achieve that?
iOS
The underlying iOS native component, UIScrollView (technically, RCTEnhancedScrollView), doesn't support keeping the scroll indicators visible. For this reason, the React Native wrapper around it won't either.
There is a hack to get this working with the native component (see this answer for one approach). To accomplish this in React Native, you'd need to implement this hack on the native side, and then either create your own Native Module or fork React Native and modify their ScrollView component.
That said, the iOS Scroll View interface guidelines discourage this, so you may want to leave the indicators' behavior alone.
Android
A few approaches:
set <item name="android:overScrollMode">always</item>,
set android:fadeScrollbars="false" in XML, or
set ScrollView.setScrollbarFadingEnabled(false) in Java (e.g. in your custom native bridge code)
This is similarly discouraged as nonstandard UI unless you have a strong reason for it.
Adding answer since none of the above worked for me.
Android now has the persistentScrollbar props.
iOS does not support this. So I created a JS solution that can be used as follows:
<SBScrollView persistentScrollbar={true}>...</SBScrollView>
Basically, this functional component will use persistentScrollbar when on Android, while add a bar when we are on iOS. It is not smooth for now, but it is functional.
// #flow
import React, {useState} from 'react';
import {Platform, View, ScrollView} from 'react-native';
type Props = {|
persistentScrollbar?: boolean,
children?: React$Node,
|} & View.propTypes;
export default function SBScrollView({
persistentScrollbar = false,
children,
...other
}: Props) {
const [nativeEvent, setNativeEvent] = useState();
if (Platform.OS === 'android' || !persistentScrollbar) {
// Abdroid supports the persistentScrollbar
return (
<ScrollView persistentScrollbar={persistentScrollbar} {...other}>
{children}
</ScrollView>
);
}
const top = nativeEvent
? nativeEvent.contentOffset.y +
(nativeEvent.contentOffset.y / nativeEvent.contentSize.height) *
nativeEvent.layoutMeasurement.height
: 0;
// iOS does not support persistentScrollbar, so
// lets simulate it with a view.
return (
<ScrollView
scrollEventThrottle={5}
showsVerticalScrollIndicator={false}
onScroll={event => setNativeEvent(event.nativeEvent)}
{...other}>
{children}
<View
style={{
position: 'absolute',
top,
right: 4,
height: 200,
width: 4,
borderRadius: 20,
backgroundColor: 'gray',
}}
/>
</ScrollView>
);
}
I hope this can help others.
I was looking for a solution but I didn't find nothing, then I created a solution, I hope can help you with it.
I created a view View with height and width and put it over my scrollview, after that I used the Props of scrollview like onMomentumScrollBegin, onMomentumScrollEnd, onContentSizeChange and onScroll
after that I make a condition with a boolean variable, if this variable is false, the View is visible, if is false the View is hide, How do I active this variable? with the Prop onMomentumScrollBegin that detect when you use the scrollView and the same way to set the variable in false with onMomentumScrollEnd that detects when the scroll ends.
The Prop onContentSizeChange allows me to get the height and width of my scrollview, this values I used to calculate where would be set the scrollbar/scrollIndicator
and finally with the Prop onScroll I get the position.
the example:
<ScrollView
onMomentumScrollBegin={() => {this.setvarScrollState()}}
onMomentumScrollEnd={() => {this.setvarScrollStateRev()}}
scrollEventThrottle={5}
onContentSizeChange={(w, h) => this.state.hScroll = h}
showsVerticalScrollIndicator={false}
onScroll={event => { this.state.wScroll = event.nativeEvent.contentOffset.y }}
style={{ marginVertical: 15, marginHorizontal:15, width: this.state.measurements3.width}}>
{
Mydata.map((value, index) => {
return <TouchableOpacity>
<Text>{ value.MyDataItem }</Text>
</TouchableOpacity>
})
}
the functions:
setvarScrollState() {
this.setState({VarScroll: true});
}
setvarScrollStateRev() {
this.setState({VarScroll: false});
}
and the variable
this.state = {VarScroll: false}
Then my condition is
!this.state.VarScroll ?
<View
style = {{
marginTop: 200*(this.state.wScroll / this.state.hScroll),
marginLeft:338.5,
height: 35,
width: 2,
backgroundColor: 'grey',
position:'absolute'
}}
/>
: null
Why 200? because is the maximum value that my marginTop can set
Check the picture
Final note:
the scrollView have to be inside a View with the another View (scrollbar)
something like this
<View>
{/*---- ScrollBar and conditions----*/}
<View>
<View>
<ScrollView>
</ScrollView>
</View>