Hi I am trying to achieve scrollview snap to center
like below gif link
Check This Gif
But unable to do so. Following is my react native code to achieve this.
or is there any method to scroll to particular index of scrollview elements like android ?
Any help would be appreciated.
Thanks in advance
<ScrollView
style={[styles.imgContainer,{backgroundColor:colorBg,paddingLeft:20}]}
automaticallyAdjustInsets={false}
horizontal={true}
pagingEnabled={true}
scrollEnabled={true}
decelerationRate={0}
snapToAlignment='center'
snapToInterval={DEVICE_WIDTH-100}
scrollEventThrottle={16}
onScroll={(event) => {
var contentOffsetX=event.nativeEvent.contentOffset.x;
var contentOffsetY=event.nativeEvent.contentOffset.y;
var cellWidth = (DEVICE_WIDTH-100).toFixed(2);
var cellHeight=(DEVICE_HEIGHT-200).toFixed(2);
var cellIndex = Math.floor(contentOffsetX/ cellWidth);
// Round to the next cell if the scrolling will stop over halfway to the next cell.
if ((contentOffsetX- (Math.floor(contentOffsetX / cellWidth) * cellWidth)) > cellWidth) {
cellIndex++;
}
// Adjust stopping point to exact beginning of cell.
contentOffsetX = cellIndex * cellWidth;
contentOffsetY= cellIndex * cellHeight;
event.nativeEvent.contentOffsetX=contentOffsetX;
event.nativeEvent.contentOffsetY=contentOffsetY;
// this.setState({contentOffsetX:contentOffsetX,contentOffsetY:contentOffsetY});
console.log('cellIndex:'+cellIndex);
console.log("contentOffsetX:"+contentOffsetX);
// contentOffset={{x:this.state.contentOffsetX,y:0}}
}}
>
{rows}
</ScrollView>
You don't need other libraries you can do that with ScrollView. All you need is to add the following props in your component.
horizontal= {true}
decelerationRate={0}
snapToInterval={200} //your element width
snapToAlignment={"center"}
Check this snack for more details on how to implement it
https://snack.expo.io/H1CnjIeDb
Use the pagingEnabled property in ScrollView.
const screenHeight = Dimensions.get('window').height;
class Screen extends Component {
constructor(props) {
super(props);
}
render() {
return (
<ScrollView pagingEnabled>
<Page1 style={{height: screenHeight}} />
<Page2 style={{height: screenHeight}} />
</ScrollView>
);
}
}
If you don't want to use snapToInterval because of the misalignment for long lists you can use snapToOffsets which is more precise.
for example:
const { width } = Dimensions.get('window');
this.IMAGE_WIDTH = width * (1 / 2.5)
this.image_margin = 5
this.nishhar = width - ((this.IMAGE_WIDTH + this.image_margin) * 2 + this.image_margin * 2)
dataNum = [1, 2, 3, 4, 5, 6]
return (<FlatList
data={dataNum}
renderItem={item => {
return (
<View style={{
backgroundColor: item.index % 2 == 0 ? 'red' : 'blue',
width: this.IMAGE_WIDTH,
height: this.IMAGE_WIDTH,
marginLeft: this.image_margin,
marginRight: this.image_margin,
}}>
</View>)
}}
keyExtractor={this.keyGenerator}
horizontal={true}
snapToAlignment={"start"}
snapToOffsets={[...Array(dataNum.length)].map((x, i) => (i * (this.IMAGE_WIDTH + 2 * this.image_margin) - (this.nishhar * 0.5)))}
decelerationRate={"fast"}
pagingEnabled
/>)
or you can also checkout this example:
https://snack.expo.io/SyWXFqDAB
There are several options. Here are two that I've tried and work fine. I prefer the second one because as its doc says "like ListView, this can render hundreds of pages without performance issue".
react-native-page-swiper
react-native-viewpager
Try using the snapToOffsets prop. This requires a simple addition and multiplication though.
To create a snapping effect showing 2 elements, and 10pts from left and right elements that are out of bound;
// import useWindowDimensions from 'react-native'
const { width: windowWidth } = useWindowDimensions();
// the width of a card in ScrollView
// 20 is so out of bound cards on left & right can have 10pts visible;
const cardWidth = (windowWidth / 2) - 20;
// gap between each cards;
const gap = 16;
const halfGap = gap / 2;
const cardContent = [1, 2, 3, 4, 5]
// THIS IS THE FUN PART
const snapOffsets = cardContent
.map((_, index) => {
return (cardWidth * index) + (halfGap * index)
})
// add left and right margin to <Card/> for gap effect,
// as well as width using cardWidth above
// conditional left margin to first card, and right margin to last card
return (
<ScrollView
horizontal={true}
snapToOffsets={snapOffsets}
>
{
cardContent.map((item, index, arr) => {
return (
<Card
key={index}
title={item}
style={{
width: cardWidth,
marginLeft: index == 0 ? gap : 0,
marginRight: index == arr.length - 1 ? gap : halfGap
}}
/>
)
})
}
</ScrollView>
)
That's it. You can refactor for vertical mode.
For a better edge to edge effect, make sure that ancestor containers don't have padding
I had problem with FlatList's snapToInterval logic on iOS (on horizontal list it didn't always snapped correctly to start) - I fixed that with increasing values of below properties (I had single item slider)
maxToRenderPerBatch={6}
initialNumToRender={6}
windowSize={6}
Related
I am trying to dynamically get the height of a table in a PDF that I created using jsPDF and js Autotable. I want to add a line of text under the table, but since the table height changes, I can't put a specific Y value for the text line.
My current table script:
let col = ['Header 1', 'Header 2', 'Header 3']
let rows = arrayForTable;
let getTableWidth = 150;
let pageWidth = doc.internal.pageSize.width;
let tableMargin = (pageWidth - getTableWidth) / 2;
let height = 0;
doc.autoTable(col, rows, {
margin: {top: 20, right: tableMargin, left: tableMargin},
startY: 70,
styles: {
halign: 'center',
overflow: 'linebreak',
cellWidth: 'wrap',
valign: 'middle'
},
columnStyles: {
0: {cellWidth: 50},
1: {cellWidth: 50},
2: {cellWidth: 50},
},
headStyles: {
fillColor: [163, 32, 32]
},
didParseCell: function(data) {
if (data.row.index === rows.length - 1) {
data.cell.styles.fillColor = [63, 63, 64];
data.cell.styles.fontStyle = 'bold';
data.cell.styles.textColor = [255, 255, 255];
}
height = data.table.height
}
});
console.log(height)
The console prints undefined. Anyone know how to dynamically add a line of text under this table without having to designate a static Y value like this:
doc.text(25, 40, exampleText);
The table height changes every time because of different data being shown.
I am using jsPDF AutoTable plugin v3.5.6.
Get the cursor position on Y axis using
didDrawPage
which will be precisely after the table.
This worked for me in Angular 2:
let yPos = 0;
pdf.autoTable({
...
didDrawPage: function(data) {
yPos = data.cursor.y;
}
}
jspdf version:
"jspdf": "^2.3.1",
"jspdf-autotable": "^3.5.14",
Source:
Table height is always 0 #604
From what I understood, you want to add the text under the table. Instead of trying to read the table height, you can read the last cell Y position. What you need to do is like this
let height = 0;
....
didParseCell: function(data) {
...
height = data.cursor.y + data.row.height;
}
....
then you can adjust the position of the text you want to add, for the example I want to add 10 gap between the table and the text
doc.text("exampleText", 25, height + 10);
const viewableWindowHeight = Dimensions.get('window').height - Header.HEIGHT - ???
How do I get the TabBar height?
What if the iPhone is X? How can I take that into account?
Solution 1
If you want to calculate viewable window height directly, then you can use the onLayout callback, for eg, on tab navigation each page,
render() {
return (
<View style={{ flex: 1}} onLayout={(event) => {
var {x, y, width, height} = event.nativeEvent.layout;
this.viewableWindowHeight=height;
// use height as viewableWindowHeight
}} />
<ScollView>
//Your scrollable contant
</ScrollView>
</View>
);
Solution 2
According to an issue in react navigation, you can't directly calculate the height of the bottom tab Bar. But if you wrap bottom tab bar into a view and then you can calculate that views height as bottom tab bar. Consider the example below
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { View } from 'react-native';
import { BottomTabBar } from 'react-navigation';
class TabBarComponent extends Component {
measure = () => {
if (this.tabBar) {
this.tabBar.measureInWindow(this.props.setTabMeasurement);
}
}
render() {
return (
<View
ref={(el) => { this.tabBar = el; }}
onLayout={this.measure}
>
<BottomTabBar {...this.props} />
</View>
);
}
}
function mapDispatchToProps(dispatch) {
return {
setTabMeasurement: (x, y, width, height) => dispatch({
type: 'SET_TAB_MEASUREMENT',
measurement: {
x, y, width, height,
},
}),
};
}
export default connect(null, mapDispatchToProps)(TabBarComponent);
Try this:
import { Dimensions, Platform } from 'react-native';
import {
getStatusBarHeight,
getBottomSpace,
} from 'react-native-iphone-x-helper';
import { Header } from 'react-navigation';
const { height } = Dimensions.get('window');
const stackHeaderHeight = Header.HEIGHT;
/* Taken from source code of react-navigation-tabs*/
const TAB_BAR_DEFAULT_HEIGHT = 49;
const TAB_BAR_COMPACT_HEIGHT = 29;
const TAB_BAR_HEIGHT = this.bottomTabBarRef._shouldUseHorizontalLabels() && !Platform.isPad
? TAB_BAR_COMPACT_HEIGHT
: TAB_BAR_DEFAULT_HEIGHT;
const marginTop = getStatusBarHeight() + stackHeaderHeight;
const marginBottom = getBottomSpace() + TAB_BAR_HEIGHT;
// < What you're after
const viewableWindowHight = height - marginTop - marginBottom;
FOR TBBAR
Height is changing between these two values >> TAB_BAR_COMPACT_HEIGHT, and TAB_BAR_DEFAULT_HEIGHT, according to a condition determined by this method:
According to react-navigation-tabs source code.
OR
You could set initialLayout to your TabNavigatorConfig as mentioned in the documentation:
initialLayout - Optional object containing the initial height and
width, can be passed to prevent the one frame delay in
react-native-tab-view rendering.
FOR IPHONE-X
You can access statusBar height, bottomSpace in Iphone-X safely though react-native-iphone-x-helper npm module
You can simply use SafeAreaView which will automatically set topBarHeight mainly for iPhoneX phones.
In the new version use
import { useHeaderHeight } from "react-navigation-stack";
console.log(useHeaderHeight());
I want to make a grid of elements with a separator with fixed width of 1 "pixel". Like in a Photo app grid for iOS.
For item width in a FlatList I use flex style, because I can not predict their width on different screens.
Code example: https://snack.expo.io/ryK_zPmEM
But on my iPhone 7 Plus (real device) i have got this result:
The separator between the second and the third cell has a style marginLeft: 1 but looks wider than the other two separators.
And this is a screenshot of the simulator for iPhone 5S:
The result is the opposite. You can also notice that the thickness of the separator between the rows is also different.
The question is: what value should be set for margin style so that separator looked the same for all screen sizes and did not change its width between the cells int the row?
Or perhaps I need to calculate the exact value for the width of the cell instead of using flex: 1?
Full code example:
const WINDOW_WIDTH = Dimensions.get('window').width
const ITEMS_PER_ROW = 4
const MARGIN_WIDTH = 1
const TOTAL_MARGIN_WIDTH = MARGIN_WIDTH * (ITEMS_PER_ROW - 1)
const CELL_SIZE = (WINDOW_WIDTH - TOTAL_MARGIN_WIDTH) / ITEMS_PER_ROW
const listData = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
export default class App extends Component {
_renderItem = ({item, index}) => {
return (
<View style={[styles.cell, (index % 4 !== 0) ? styles.cellMargin : {}]}>
<Text>{item}</Text>
</View>
)
}
render() {
return (
<View style={styles.container}>
<FlatList
contentContainerStyle={styles.list}
columnWrapperStyle={styles.listColumn}
data={listData}
numColumns={ITEMS_PER_ROW}
keyExtractor={i => i}
renderItem={item => this._renderItem(item)}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'red',
},
list: {
backgroundColor: 'cyan',
flex: 1,
},
listColumn: {
marginTop: 1,
height: CELL_SIZE,
backgroundColor: 'white',
},
cell: {
backgroundColor: 'green',
flex: 1,
},
cellMargin: {
marginLeft: MARGIN_WIDTH,
}
});
For my current project, I'm creating a Diary/Calendar type component that shows the current day (Diary view) or current month (Calendar view) centered when the user clicks to see that view.
I'm using a ScrollView to hold my content:
_getInitialOffset(h) {
const FIX = 75; //TODO: Make it less arbitrary
let percentToScroll = (this.props.viewedDate.month()+1)/12; //12 = number of months per year
let { height } = this.props;
let centerFix = height/2;
let contentHeight = h;
let yPos = contentHeight*percentToScroll - centerFix - FIX;
return (yPos > 0) ? yPos : 0;
}
render() {
var year = this.props.viewedDate.year();
var cal = Calendar.get(year);
var contentHeight = this.contentHeight;
return (
<View>
<View style={styles.daysOfWeek}>
{
Calendar.days().map(function(day, i) {
return <Text key={i} style={styles.dayHeader}>{day}</Text>;
})
}
</View>
<ScrollView
ref={ref => {this._scrollView = ref}}
style={{
height: this.props.height,
paddingTop: 15
}}
onContentSizeChange={(w, h) => {
this._scrollView.scrollTo({y: this._getInitialOffset(h), animated: false});
}}>
<Year year={year} calendar={cal}/>
</ScrollView>
</View>
);
}
I'm trying to have it center upon render on the current month, but because my current method (using OnContentSizeChange) occurs after render, there's a frame where the user sees it uncentered, which is bad user experience.
Is there a way to get the content height of a ScrollView component before/during render, or delay component visibility until after the onContentSizeChange method has fired?
onContentSizeChange is internally using onLayout which is triggered as soon as the layout has been computed.
This event is fired immediately once the layout has been calculated, but the new layout may not yet be reflected on the screen at the time the event is received, especially if a layout animation is in progress.
There's no way to get the size before that.
So what you can do is set the opacity of your scrollview to 0 until the first onContentSizeChange is triggered.
I advise you to use Animated.Value to do the opacity change so it doesn't re-render your whole component.
My solution is as follows:
Create a parent container, with scrollOffset animation that tracks the amount of scroll.
Get the height of the ScrollView container ("containerHeight")
Get the height of the contents within the ScrollView ("contentHeight")
Use the ScrollView's "onScroll" event to get the scroll offset. Use it to update the scrollOffset animation.
Create a ScrollBar component based on the scrollOffset animation value, containerHeight, and contentHeight
The following is extracted from my code. It is incomplete and untested, but should be enough to for one to get started.
class CustomScrollView extends React.Component {
constructor(props) {
super(props);
this.state = {
scrollOffsetAnim: new Animated.Value(0)
};
}
renderScrollBar() {
const {
scrollOffsetAnim,
contentHeight,
containerHeight,
} = this.state;
//...
}
render() {
return (
<View>
<ScrollView
scrollEventThrottle={16}
onScroll={(evt) => {
const {contentOffset} = evt.nativeEvent;
const {y} = contentOffset;
this.state.scrollOffsetAnim.setValue(y);
}}
onLayout={(evt) => {
const {height} = evt.nativeEvent.layout;
const {containerHeight} = this.state;
if(!containerHeight || containerHeight !== height) {
this.setState({containerHeight: height})
}
}}
>
<View
onLayout={(evt) => {
const {height} = evt.nativeEvent.layout;
const {contentHeight} = this.state;
if(!contentHeight || contentHeight !== height) {
this.setState({contentHeight: height})
}
}}
>
{this.props.children}
</View>
</ScrollView>
{this.renderScrollBar()}
</View>
);
}
}
If you are trying to get the total height of the insides of a scrollview, just add a generic View with the onLayout prop inside it:
<ScrollView style={{flex:1}} >
<View style ={{flex:1}} onLayout={e=>console.log('e',e.nativeEvent.layout)}>
{YOUR VIEWS HERE}
</View>
</ScrollView>
Every time the component rerenders, it will print the console log ('e') with the informations you probably wishes
I would like to ask how react native handle or do the responsive font. For example in iphone 4s i Have fontSize: 14, while in iphone 6 I have fontSize: 18.
You can use PixelRatio
For example:
var React = require('react-native');
var {StyleSheet, PixelRatio} = React;
var FONT_BACK_LABEL = 18;
if (PixelRatio.get() <= 2) {
FONT_BACK_LABEL = 14;
}
var styles = StyleSheet.create({
label: {
fontSize: FONT_BACK_LABEL
}
});
Edit:
Another example:
import { Dimensions, Platform, PixelRatio } from 'react-native';
const {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
} = Dimensions.get('window');
// based on iphone 5s's scale
const scale = SCREEN_WIDTH / 320;
export function normalize(size) {
const newSize = size * scale
if (Platform.OS === 'ios') {
return Math.round(PixelRatio.roundToNearestPixel(newSize))
} else {
return Math.round(PixelRatio.roundToNearestPixel(newSize)) - 2
}
}
Usage:
fontSize: normalize(24)
You can go one step further by allowing sizes to be used on every <Text /> components by pre-defined sized.
Example:
const styles = {
mini: {
fontSize: normalize(12),
},
small: {
fontSize: normalize(15),
},
medium: {
fontSize: normalize(17),
},
large: {
fontSize: normalize(20),
},
xlarge: {
fontSize: normalize(24),
},
};
We use a simple, straight-forward, scaling utils functions we wrote:
import { Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window');
//Guideline sizes are based on standard ~5" screen mobile device
const guidelineBaseWidth = 350;
const guidelineBaseHeight = 680;
const scale = size => width / guidelineBaseWidth * size;
const verticalScale = size => height / guidelineBaseHeight * size;
const moderateScale = (size, factor = 0.5) => size + ( scale(size) - size ) * factor;
export {scale, verticalScale, moderateScale};
Saves you some time doing many ifs. You can read more about it on my blog post.
Edit: I thought it might be helpful to extract these functions to their own npm package, I also included ScaledSheet in the package, which is an automatically scaled version of StyleSheet.
You can find it here: react-native-size-matters.
adjustsFontSizeToFit and numberOfLines works for me. They adjust long email into 1 line.
<View>
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={{textAlign:'center',fontSize:30}}
>
{this.props.email}
</Text>
</View>
Because responsive units aren't available in react-native at the moment, I would say your best bet would be to detect the screen size and then use that to infer the device type and set the fontSize conditionally.
You could write a module like:
function fontSizer (screenWidth) {
if(screenWidth > 400){
return 18;
}else if(screenWidth > 250){
return 14;
}else {
return 12;
}
}
You'll just need to look up what the default width and height are for each device. If width and height are flipped when the device changes orientation you might be able to use aspect ratio instead or just figure out the lesser of the two dimensions to figure out width.
This module or this one can help you find device dimensions or device type.
I managed to overcome this by doing the following.
Pick the font size you like for the current view you have (Make sure
it looks good for the current device you are using in the
simulator).
import { Dimensions } from 'react-native' and define the width
outside of the component like so: let width = Dimensions.get('window').width;
Now console.log(width) and write it down. If your good looking font
size is 15 and your width is 360 for example, then take 360 and
divide by 15 ( = 24). This is going to be the important value that
is going to adjust to different sizes.
Use this number in your styles object like so: textFontSize: { fontSize = width / 24 },...
Now you have a responsive fontSize.
import { Dimensions } from 'react-native';
const { width, fontScale } = Dimensions.get("window");
const styles = StyleSheet.create({
fontSize: idleFontSize / fontScale,
});
fontScale get scale as per your device.
Take a look at the library I wrote: https://github.com/tachyons-css/react-native-style-tachyons
It allows you to specify a root-fontSize (rem) upon start, which you can make dependent of your PixelRatio or other device-characteristics.
Then you get styles relative to your rem, not only fontSize, but paddings etc. as well:
<Text style={[s.f5, s.pa2, s.tc]}>
Something
</Text>
Expanation:
f5is always your base-fontsize
pa2 gives you padding relative to your base-fontsize.
I simply use the ratio of the screen size, which works fine for me.
const { width, height } = Dimensions.get('window');
// Use iPhone6 as base size which is 375 x 667
const baseWidth = 375;
const baseHeight = 667;
const scaleWidth = width / baseWidth;
const scaleHeight = height / baseHeight;
const scale = Math.min(scaleWidth, scaleHeight);
export const scaledSize =
(size) => Math.ceil((size * scale));
Test
const size = {
small: scaledSize(25),
oneThird: scaledSize(125),
fullScreen: scaledSize(375),
};
console.log(size);
// iPhone 5s
{small: 22, oneThird: 107, fullScreen: 320}
// iPhone 6s
{small: 25, oneThird: 125, fullScreen: 375}
// iPhone 6s Plus
{small: 28, oneThird: 138, fullScreen: 414}
We can use flex layout and use adjustsFontSizeToFit={true} for responsive font sizes.And the text would adjust according to the size of the container.
<Text
adjustsFontSizeToFit
style={styles.valueField}>{value}
</Text>
But in styles you need to put a fontsize as well only then will adjustsFontSizeToFit work.
valueField: {
flex: 3,
fontSize: 48,
marginBottom: 5,
color: '#00A398',
},
Why not using PixelRatio.getPixelSizeForLayoutSize(/* size in dp */);, it's just the same as pd units in Android.
I'm usually using this :
import React from 'react';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
var heightY = Dimensions.get("window").height;
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.textStyle}>fontSize {heightY * 0.014}</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
textStyle: {
fontSize: heightY * 0.014,
}
})
The idea is to get the fontSize depending on height of your screen. Example calculation:
// Height 785,.. -> fontSize = 11
// Height 1000 -> fontSize = 14
// Height 1285,.. -> fontSize = 18
You can also try using this if you want it to depend on your screen width:
var widthX = Dimensions.get("window").width;
I recently ran into this problem and ended up using react-native-extended-stylesheet
You can set you rem value and additional size conditions based on screen size. As per the docs:
// component
const styles = EStyleSheet.create({
text: {
fontSize: '1.5rem',
marginHorizontal: '2rem'
}
});
// app entry
let {height, width} = Dimensions.get('window');
EStyleSheet.build({
$rem: width > 340 ? 18 : 16
});
Need to use this way I have used this one and it's working fine.
react-native-responsive-screen
npm install react-native-responsive-screen --save
Just like I have a device 1080x1920
The vertical number we calculate from height **hp**
height:200
200/1920*100 = 10.41% - height:hp("10.41%")
The Horizontal number we calculate from width **wp**
width:200
200/1080*100 = 18.51% - Width:wp("18.51%")
It's working for all device
A slightly different approach worked for me :-
const normalize = (size: number): number => {
const scale = screenWidth / 320;
const newSize = size * scale;
let calculatedSize = Math.round(PixelRatio.roundToNearestPixel(newSize))
if (PixelRatio.get() < 3)
return calculatedSize - 0.5
return calculatedSize
};
Do refer Pixel Ratio as this allows you to better set up the function based on the device density.
You can use something like this.
var {height, width} = Dimensions.get('window');
var textFontSize = width * 0.03;
inputText: {
color : TEXT_COLOR_PRIMARY,
width: '80%',
fontSize: textFontSize
}
Hope this helps without installing any third party libraries.