Accordion or collapsible component in nativebase.io - react-native

How can I create a collapsible or accordion component with using 'only' native-base library.

Here is solution that worked for me:
'use strict'
import React from 'react';
import {
AppRegistry,
Animated,
Dimensions,
Easing,
Image,
ImageBackground,
Text,
View,
StyleSheet,
TouchableOpacity,
ScrollView
} from 'react-native';
const categoryLinks = [ //for the sake of simplicity, we use the same set of category links for all touts
{label: 'Subcategory1'},
{label: 'Subcategory2'},
{label: 'Subcategory3'},
{label: 'Subcategory4'},
{label: 'Subcategory5'},
{label: 'Subcategory6'},
{label: 'Subcategory7'},
{label: 'Subcategory8'},
{label: 'Subcategory9'},
{label: 'Subcategory10'},
]
const categoryTouts = [ //the touts are the clickable image items that hold our links
{image: require('../assets/images/some_image.png'), title: 'Category1', links: categoryLinks},
{image: require('../assets/images/some_image.png'), title: 'Category2', links: categoryLinks},
{image: require('../assets/images/some_image.png'), title: 'Category3', links: categoryLinks},
]
const SUBCATEGORY_FADE_TIME = 400 //time in ms to fade in / out our subcategories when the accordion animates
const SUBCATEGORY_HEIGHT = 40 //to save a costly measurement process, we know our subcategory items will always have a consistent height, so we can calculate how big the overall subcategory container height needs to expand to by multiplying this by the number of items
const CONTAINER_PADDING_TOP = 40 //to leave room for the device battery bar
const categoryLinksLength = categoryLinks.length //number of subcategory items - if we werent using the same set of links for all touts, we would need to store this within each tout class probably, to know how big each container should expand to to show all the links
const subcategoryContainerHeight = categoryLinksLength * SUBCATEGORY_HEIGHT //total height for the container
export class CategoryLinks extends React.PureComponent { //using PureComponent will prevent unnecessary renders
toutPositions = [] //will hold the measured offsets of each tout when unexpanded
render() {
return (
<Animated.View //view should be animated because its opacity will change
style={{position: 'absolute', top: 0, left: 0, opacity: this.props.subcategoryOpacity}}
>
<View>
{
this.props.links && this.props.links.map((link, index, links) => { //render our subcategory links
return (
<View
key={link.label}
>
<Text style={styles.subcategoryLinks}>{link.label}</Text>
</View>
)
})
}
</View>
</Animated.View>
)
}
}
export class Tout extends React.PureComponent { //using PureComponent will prevent unnecessary renders
state = {
toutSubcategoriesVisible: false, //true when we the tout has been clicked on and subcategory items are exposed
}
animatedValue = new Animated.Value(0) //we will animate this value between 0 and 1 to hide and show the subcategories
animCategoryHeight = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, subcategoryContainerHeight], //when animated value is 1, the subcategory container will be equal to the number of links * each links height
})
subcategoryOpacity = new Animated.Value(0) //separate animated value for each subcategory list's opacity, as we will animate this independent of the height
measurements = {} //will hold each tout's location on the page so that we can automatically scroll it to the top of our view
measureToutRef = () => {
this.toutRef.measure((x, y, width, height, pageX, pageY) => { //measuring gives us all of these properties, so we must capture them and pass down only the two we need
this.measurements.pageY = pageY //Y position in the overall view
this.measurements.height = height //height of the tout (really this is the same among all touts in our example, but we will allow our touts to have different heights this way)
this.props.handleLayout(this.measurements, this.props.toutIndex) //pass this back to the parent (scrollAccordion)
})
}
handlePressTout = () => {
if (this.props.links && this.props.links.length) { //if the tout has subcategory links, hide or show them based on the current state
const toutSubcategoriesVisible = this.state.toutSubcategoriesVisible
if (toutSubcategoriesVisible) {
this.hideToutSubcatgories()
}
else {
this.showToutSubcatgories()
}
}
}
showToutSubcatgories = () => {
this.setState({toutSubcategoriesVisible: true})
Animated.timing(this.animatedValue, { //animating this value from zero to one will update the subcategory container height, which interpolates this value
toValue: 1,
duration: SUBCATEGORY_FADE_TIME,
easing: Easing.inOut(Easing.quad),
}).start(() => {
this.props.handlePressTout(this.props.toutIndex)
})
Animated.timing(this.subcategoryOpacity, {
toValue: 1,
duration: SUBCATEGORY_FADE_TIME,
easing: Easing.inOut(Easing.quad),
}).start()
}
hideToutSubcatgories = () => {
Animated.timing(this.animatedValue, {
toValue: 0,
duration: SUBCATEGORY_FADE_TIME,
easing: Easing.inOut(Easing.quad),
}).start(() => {
this.setState({toutSubcategoriesVisible: false})
})
Animated.timing(this.subcategoryOpacity, {
toValue: 0,
duration: SUBCATEGORY_FADE_TIME,
easing: Easing.inOut(Easing.quad),
}).start()
}
setToutRef = node => { //store a reference to the tout so we can measure it
if (node) {
this.toutRef = node
}
}
render() {
let categoryLinks
if (this.props.links && this.props.links.length) { //if the tout has links, render them here
categoryLinks = (
<Animated.View
style={{height: this.animCategoryHeight}}
>
<CategoryLinks {...this.props} isVisible={this.state.toutSubcategoriesVisible}
subcategoryOpacity={this.subcategoryOpacity}/>
</Animated.View>
)
} else {
categoryLinks = null
}
return (
<View
style={this.props.toutIndex === 0 ? {marginTop: 0} : {marginTop: 5}} //if this is the first tout, no margin is needed at top
onLayout={!this.measurements.pageY ? this.measureToutRef : () => null} //if we already have measurements for this tout, no need to render them again. Otherwise, get the measurements
>
<TouchableOpacity
ref={this.setToutRef}
onPress={this.handlePressTout}
>
<ImageBackground
source={this.props.image}
style={styles.toutImage}
width={'100%'}
>
<Text
style={styles.toutText} //text is wrapped by image so it can be easily centered
>
{this.props.title}
</Text>
</ImageBackground>
</TouchableOpacity>
{categoryLinks}
</View>
)
}
}
AppRegistry.registerComponent('Tout', () => Tout);
export default class scrollAccordion extends React.PureComponent { //scroll accordion is our parent class - it renders the touts and their subcategories
measurements = []
handlePressTout = (toutIndex) => { //when we press a tout, animate it to the top of the screen and reveal its subcategoires
this.scrollViewRef.scrollTo({
y: this.measurements[toutIndex].pageY - CONTAINER_PADDING_TOP,
})
}
setScrollRef = node => { //store a reference to the scroll view so we can call its scrollTo method
if (node) {
this.scrollViewRef = node
}
}
handleLayout = (measurements, toutIndex) => { //this process is expensive, so we only want to measure when necessary. Probably could be optimized even further...
if (!this.measurements[toutIndex]) { //if they dont already exist...
this.measurements[toutIndex] = measurements //...put the measurements of each tout into its proper place in the array
}
}
render() {
console.log('render')
return (
<View style={styles.container}>
<ScrollView
scrollEventThrottle={20} //throttling the scroll event will decrease the amount of times we store the current scroll position.
ref={this.setScrollRef}
>
<View>
{
categoryTouts.map((tout, index) => {
return (
<Tout
key={index}
toutIndex={index} //tout index will help us know which tout we are clicking on
{...tout}
handleLayout={this.handleLayout} //when layout is triggered for touts, we can measure them
handlePressTout={this.handlePressTout}
/>
)
})
}
</View>
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: CONTAINER_PADDING_TOP,
backgroundColor: 'white',
},
toutText: {
color: 'white', backgroundColor: 'transparent', fontWeight: 'bold', fontSize: 24
},
toutImage: {
alignItems: 'center', justifyContent: 'center'
},
subcategoryLinks: {
lineHeight: 40,
}
});
AppRegistry.registerComponent('scrollAccordion', () => scrollAccordion);

Related

Why does this not display anything on screen - warning about unique key props (React Native)

I am creating an app and experimenting with components and most of all, animations.
I have the below code with two class components:
import * as React from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Image, ImageBackground, Animated, Easing, Platform
} from 'react-native';
import { frame1 } from '../master-new/assets/index';
import { frame2 } from '../master-new/assets/index';
import { frame3 } from '../master-new/assets/index';
import { background } from '../master-new/assets/index';
const Images= [
{ id: 1, src: frame1, title: 'foo', description: 'bar'},
{ id: 2, src: frame2, title: 'foo', description: 'bar'},
]
const length = Images.length;
class Animation extends React.Component {
constructor(){
super();
this.animations = new Animated.Value(0);
this.opacity = [];
Images.map((item, index) => {
this.opacity.push(
this.animations.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [0, 1, 0],
}),
);
});
}
componentDidMount() {
Animated.loop(
Animated.timing(this.animations, {
toValue: length - 1,
duration: 2000 * length,
easing: Easing.linear,
useNativeDriver: true,
}),
).start();
}
render() {
return(
<View style={styles.container}>
{Images.map((item, index) => {
const opacity = this.opacity[index];
return (
<Animated.View
style={[styles.anim, {frame: item, opacity}]}
/>
);
})}
</View>
)
}
}
export default class Timer extends React.Component {
constructor(props){
super(props);
this.state = {
time:0,
start:0,
isOn:false,
submit:false,
scoreArray: [],
animalArray: [],
fadeAnim: new Animated.Value(0),
pauseOver: false,
pauseToggle: 'up',
}
}
sampleInfo = [
{
second: 1,
ml: '19ml',
animal: 'Thing1',
weight: '4kg',
capacity: '20ml'
},
{
second: 2,
ml: '38ml',
animal: 'Thing2',
weight: '7kg',
capacity: '35ml'
},
{
second: 3,
ml: '57ml',
animal: 'Thing3',
weight: '12kg',
capacity: '60ml'
}
]
render() {
return(
<View style={styles.container}>
<Animation />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "column"
},
background: {
flex: 1,
resizeMode: "cover",
justifyContent: "center"
},
anim: {
flex: 1,
width: '100%'
}
});
I'm displaying with expo and have successfully displayed apps before and tested them in action. Can someone tell me why I'm seeing just a blank screen for this one?
I'm getting a warning saying Each chile in a list should have a unique key prop. Check the render method of animation, so I guess that's where the issue lies but why and is it the cause of just a white screen?
I've read: Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `ListView`
and:
https://reactjs.org/docs/lists-and-keys.html
but it didn't clear anything up for me!
T
react need a way to identify the nodes inside the UI, when you render a list/array, you need to provide the key attribute to the component, in your case it is when you render the images array, that is when react complains about that, react needs this to know what item to update if an update in the state was made.
to slove this issue, simply add the key attribuite to the component, make sure the value is unique, either an id or the index of the item
render() {
return(
<View style={styles.container}>
{Images.map((item, index) => {
const opacity = this.opacity[index];
return (
<Animated.View
key = {item.id}
style={[styles.anim, {frame: item, opacity}]}
/>
);
})}
</View>
)
}
PS:
only use index as a last resort, in your case you have ids, which is guaranteed to be unique, index however, is not guaranteed.
You need to put key attributes on the JSX elements returned in a loop. Like:
{Images.map((item, index) => {
const opacity = this.opacity[index];
return (
<Animated.View
key={index}
style={[styles.anim, {frame: item, opacity}]}
/>
);
})}

automatic scrolling in code for PDF using React Native and react-native-pdf

I'm a bit new to React Native programming, and I'm working on implementing an autoscroll feature for PDFs. For example, in some cases, I want to automatically scroll a PDF down x pages and then scroll at a desired speed. I followed this tutorial here here which works for just normal data, but when I used a object from react-native-pdf, it does not seem to scroll anymore. I'm wrapping the PDF object inside a ScrollView and can confirm that the scrolling code is being called. Can anyone suggest a solution or explain why this does not work with the PDF? Thanks so much!
I've also attached my code below if that helps. Currently, the PDF displays but is not autoscrolling at all.
import React from 'react';
import {StyleSheet, Dimensions, View, ScrollView} from 'react-native';
import Pdf from 'react-native-pdf';
export default class PDFScroll extends React.Component {
constructor(props) {
super(props);
this.state = {
currentPosition: 0,
};
this.scrolling = this.scrolling.bind(this);
}
componentDidMount(){
this.activeInterval = setInterval(this.scrolling, 100);
}
componentWillUnmount(){
clearInterval(this.activeInterval);
}
scrolling() {
position = this.state.currentPosition + 50;
this.pdf.scrollTo({ x: position, animated: true });
// After position passes this value, snaps back to beginning
let maxOffset = 2000;
// Set animation to repeat at end of scroll
if (this.state.currentPosition > maxOffset) {
this.pdf.scrollTo({ x: 0, animated: false })
this.setState({ currentPosition: 0 });
}
else {
this.setState({ currentPosition: position });
}
}
render() {
const source = {
uri: 'http://samples.leanpub.com/thereactnativebook-sample.pdf',
cache: true,
};
return (
<View style={styles.container}>
<ScrollView
style={styles.scrollview}
horizontal={false}
bounces={true}
ref={(ref) => this.pdf = ref}
>
{
<Pdf
source={source}
onLoadComplete={(numberOfPages, filePath) => {
console.log(`number of pages: ${numberOfPages}`);
}}
onPageChanged={(page, numberOfPages) => {
console.log(`current page: ${page}`);
}}
onError={error => {
console.log(error);
}}
onPressLink={uri => {
console.log(`Link presse: ${uri}`);
}}
style={styles.pdf}
/>
}
</ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
marginTop: 25,
},
pdf: {
flex: 1,
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
},
});

How to display a loader icon, while the page is rendered in React-native?

I am actually rendering a Complex UI in a react-native app. I am using react-navigation. Whenever I click the option in navigation drawer for my complex UI Page, the whole app hangs for 3-5 seconds and then the page is shown. What I want is a loader screen that loads immediately when I click on the option in navigation drawer and when the complex UI is rendered the loader should disappear and the UI should be shown. The app freezes because of the rendering of the UI. Is there any way to asynchronously render the UI after displaying the loading screen?
Edit
Below is the complex UI that I mentioned earlier. This table is loaded when I navigate to this page.
// source https://snack.expo.io/#shrey/highly-responsive-sheet
import React from "react"
import { Animated, ActivityIndicator, FlatList, ScrollView, StyleSheet, Text, View,TouchableOpacity } from "react-native"
const NUM_COLS = 15
const NUM_ROWS_STEP = 20
const CELL_WIDTH = 100
const CELL_HEIGHT = 60
const black = "#000"
const white = "#fff"
const styles = StyleSheet.create({
container: { backgroundColor: white, marginVertical: 40, marginBottom: 80 },
header: { flexDirection: "row", borderTopWidth: 1, borderColor: black },
identity: { position: "absolute", width: CELL_WIDTH },
body: { marginLeft: CELL_WIDTH },
cell: {
width: CELL_WIDTH,
height: CELL_HEIGHT,
borderRightWidth: 1,
borderBottomWidth: 1,
borderColor: black,
},
column: { flexDirection: "column" },
})
class Sheet extends React.Component {
constructor(props: {}) {
super(props)
this.headerScrollView = null
this.scrollPosition = new Animated.Value(0)
this.scrollEvent = Animated.event(
[{ nativeEvent: { contentOffset: { x: this.scrollPosition } } }],
{ useNativeDriver: false },
)
this.state = { count: NUM_ROWS_STEP, loading: false }
}
handleScroll = e => {
if (this.headerScrollView) {
let scrollX = e.nativeEvent.contentOffset.x
this.headerScrollView.scrollTo({ x: scrollX, animated: false })
}
}
scrollLoad = () => this.setState({ loading: false, count: this.state.count + NUM_ROWS_STEP })
handleScrollEndReached = () => {
if (!this.state.loading) {
this.setState({ loading: true }, () => setTimeout(this.scrollLoad, 500))
}
}
formatCell(value) {
return (
<TouchableOpacity onPress=()>
<View key={value} style={styles.cell}>
<Text>{value}</Text>
</View>
</TouchableOpacity>
)
}
formatColumn = (section) => {
let { item } = section
let cells = []
for (let i = 0; i < this.state.count; i++) {
cells.push(this.formatCell(`col-${i}-${item.key}`))
}
return <View style={styles.column}>{cells}</View>
}
formatHeader() {
let cols = []
for (let i = 0; i < NUM_COLS; i++) {
cols.push(this.formatCell(`frozen-row-${i}`))
}
return (
<View style={styles.header}>
{this.formatCell("frozen-row")}
<ScrollView
ref={ref => (this.headerScrollView = ref)}
horizontal={true}
scrollEnabled={false}
scrollEventThrottle={16}
>
{cols}
</ScrollView>
</View>
)
}
formatIdentityColumn() {
let cells = []
for (let i = 0; i < this.state.count; i++) {
cells.push(this.formatCell(`frozen-col-${i}`))
}
return <View style={styles.identity}>{cells}</View>
}
formatBody() {
let data = []
for (let i = 0; i < NUM_COLS; i++) {
data.push({ key: `content-${i}`})
}
return (
<View>
{this.formatIdentityColumn()}
<FlatList
style={styles.body}
horizontal={true}
data={data}
renderItem={this.formatColumn}
stickyHeaderIndices={[0]}
onScroll={this.scrollEvent}
scrollEventThrottle={16}
extraData={this.state}
/>
</View>
)
}
formatRowForSheet = (section) => {
let { item } = section
return item.render
}
componentDidMount() {
this.listener = this.scrollPosition.addListener(position => {
this.headerScrollView.scrollTo({ x: position.value, animated: false })
})
}
render () {
let body = this.formatBody()
let data = [{ key: "body", render: body }]
return (
<View style={styles.container}>
{this.formatHeader()}
<FlatList
data={data}
renderItem={this.formatRowForSheet}
onEndReached={this.handleScrollEndReached}
onEndReachedThreshold={.005}
/>
{this.state.loading && <ActivityIndicator />}
</View>
)
}
}
export default Sheet
Your UI probably also loads slowly because you are using a FlatList inside a FlatList. In my experience it will only cause confussion and performance issues.
One thing you might also want to do is integrate with something like Redux, to handle a global loading state, and based on that value you show a loading spinner or the data.
Without seeing actual code, I can only suggest high-level solutions:
Consider using requestAnimationFrame or InteractionManager to schedule expensive calculations.
Render the loading state first, then listen to navigation focus event to start rendering your Complex UI.
Remember to test in production mode, because the difference with development can be signification.
Links to the concepts I mentioned:
https://facebook.github.io/react-native/docs/performance#my-touchablex-view-isn-t-very-responsive
https://facebook.github.io/react-native/docs/timers#interactionmanager
https://reactnavigation.org/docs/en/navigation-events.html

Animate bar chart issue in React Native

I'm doing a bar chart and the issue is that it loads to the downside. I just need to make it correct side. How can I solve that issue?
Here I attached a current image of the graph:
Here is the index.js and I used child component AnimatedBar to draw each bar column. I generated random heights and passed them to the child component.
componentDidMount() {
this.generateData();
// this.interval = setInterval(() => {
// this.generateData();
// }, 1000); }
componentWillUnmount() {
clearInterval(this.interval); }
generateData = () => {
const data = [];
for (let i = 0; i < 10; i++) {
data.push(Math.floor(Math.random() * window.width));
}
this.setState({
data,
}); }
render() {
return (
<View style={{ flex: 1,flexDirection:"column", backgroundColor: '#F5FCFF', justifyContent: 'center'}}>
<View style={{flexDirection:"row",justifyContent: 'flex-end'}}>
{this.state.data.map((value, index) => <AnimatedBar value={value} delay={DELAY * index} key={index} />)}
</View>
</View>
); }
Here I added the child component:
class AnimatedBar extends Component {
constructor(props) {
super(props);
this._width = new Animated.Value(0);
this.state = {
color: randomcolor(),
};
}
componentDidMount() {
this.animateTo( this.props.value);
}
componentWillReceiveProps(nextProps) {
this.animateTo( nextProps.value);
}
animateTo = (value) => {
// Animated.sequence([
// Animated.delay(delay),
// Animated.timing(this._width, {
// toValue: value,
// }),
// ]).start();
Animated.timing(this._width, {
toValue: value,
}).start();
}
render() {
const barStyles = {
backgroundColor: this.state.color,
height: this._width,
width:40,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
};
return (
<Animated.View style={barStyles} />
);
}
}
Animated.timing(this._width, {
toValue: value,
}).start();
If you will pass the positive value to the toValue variable then bar will go downward and if you will pass the negative value toValue variable then bar will go upward.
Try this by passing negative value to the toValue like :
Animated.timing(this._width, {
toValue: -30,
}).start();

How to change Text in Animation in react-native?

I use Animated.Text for change Animation Text but it's not working properly
I also require in animation fade out old text & fade in the new text.
import React, { Component, PropTypes } from 'react';
import {
StyleSheet,
View,
Text,
Image,
Dimensions,
Animated
} from 'react-native';
import styles from './styles';
const win = Dimensions.get('window');
export default class Logo extends Component {
constructor(props) {
super(props);
this.tempText = new Animated.Value("Hello");
}
componentWillMount () {
Animated.timing(this.tempText, {
duration: 5000,
toValue: "New Text",
}).start();
};
render() {
return (
<View style={{flex:1}}>
<Animated.Text style={{color: "#9b9b9b"}}>
{this.tempText}
</Animated.Text>
</View>
);
}
}
Actual output Get - Change text after 5 Second but it's not working.please help me.
What you're trying to achieve can be done without using Animated at all, and actually, Animated isn't intended for this particular use.
Replacing the text can be done with a simple variable, and the text change can be triggered by a setTimeout.
Animated is intended for changing a numeric value, not a text value. Think of it this way - if the change is supposed to happen over a 5 second interval, what would the mid-value be?
Do this instead:
export default class Logo extends Component {
constructor(props) {
super(props);
this.state = {text: "Hello"};
}
componentDidMount () {
setTimeout(() => this.setState({text: "New Text"}), 5000);
};
render() {
return (
<View style={{flex:1}}>
<Animated.Text style={{color: "#9b9b9b"}}>
{this.state.text}
</Animated.Text>
</View>
);
}
}
My example with smoothly opacity animation.
Sorry, without fadeIn, fadeOut.
const inputRange = [0, 1, 2, 3];
const AnimatedText = Animated.createAnimatedComponent(Text);
const animationProps = {
duration: 500,
easing: Easing.out(Easing.linear),
isInteraction: false,
useNativeDriver: true,
};
class Onboarding extends PureComponent {
activeDot = 0;
animationDot = new Animated.Value(0);
toggleOnButton = () => {
Animated.timing(this.animationDot, {
toValue: this.activeDot + 1,
...animationProps,
}).start((endState) => {
if (endState.finished) {
this.activeDot = this.activeDot + 1;
}
});
}
renderButton = () => {
const opacityNext = this.animationDot.interpolate({
inputRange,
outputRange: [1, 1, 1, 0]
});
const opacityGetStarted = this.animationDot.interpolate({
inputRange,
outputRange: [0, 0, 0, 1]
});
return (
<TouchableOpacity style={styles.button} onPress={this.toggleOnButton}>
<AnimatedText style={[styles.buttonText, { opacity: opacityNext }]}>
Next
</AnimatedText>
<AnimatedText style={[styles.buttonText, {
top: normalize(isIOS ? 12 : 8), position: 'absolute', opacity: opacityGetStarted
}]}
>
Get started
</AnimatedText>
</TouchableOpacity>
);
}
}