Search code examples
cssreactjsreact-nativehybrid-mobile-app

React Native ScrollView not scrolling to the bottom


So I have a slightly unique case here and I am having a lot of trouble figuring out the flexbox layouts. I basically have one "header" that stays fixed at the top ("headerContainer") then a ScrollView below it that wraps a HTMLView container (https://github.com/jsdf/react-native-htmlview).

The problem is that the ScrollView won't scroll to the bottom of the HTMLView content. I had tried adding a margin to the HTMLView but the amount extra that I had to scroll seems to be variable so sometimes I'll get a big gap between how much I can scroll and the content. You will also see that I have a TouchableOpacity below the HTMLView and that again is too far down for the ScrollView to reach. Any suggestions on how to change my styles to get this to work?

const visitWebsiteVersion = (url) => {
    OutOfAppLinking(url)
}
const PostView = (props) => {
    let postTypeName;
    let postIcon;
    switch (props.postType) {
        case 'something1':
            postTypeName = 'something1'
            postIcon = <FontAwesome name={'bell'} size={20} color={gray500} style={styles.iconStyles}/>
            break;
        case 'something2':
            postTypeName = 'Something2'
            postIcon = <FontAwesome name={'newspaper-o'} size={15} color={gray500} style={styles.iconStyles}/>
            break;
        case 'something3':
            postTypeName = 'Something3'
            postIcon = <FontAwesome name={'book'} size={15} color={gray500} style={styles.iconStyles}/>
            break;
        case 'something4':
            postTypeName = 'Something4'
            postIcon = <FontAwesome name={'book'} size={15} color={gray500} style={styles.iconStyles}/>
            break;
        default:
            postTypeName = ''
            postIcon = ''
    }

    return(
        <View style={styles.postContainer}>
            <View style={styles.headerContainer}>
                <View style={styles.headerTopRow}>
                    <View style={{flexDirection: 'row'}}>
                        {postIcon}
                        <Text style={styles.postType}>{postTypeName}</Text>
                    </View>
                    <Text style={styles.postDate}>{monthDayYearConversion(props.postDate)}</Text>
                </View>
                <Text style={styles.postTitle}>{props.title}</Text>
                <View style={styles.headerBottomRow}>
                    <View>
                        <View style={{flexDirection: 'row'}}>
                            <Text style={styles.authorName}>By {props.author.name}</Text>
                        </View>
                        <View style={{flexDirection: 'row'}}>
                            <Text style={styles.subscriptionTitle}>{props.subscription}</Text>
                        </View>
                    </View>
                </View>
            </View>
            <ScrollView
                style={styles.scrollContainer}
                showsVerticalScrollIndicator={false}
            >
                <HTMLView
                    value={props.content}
                    stylesheet={htmlContentStylesheet}
                    style={styles.contentContainer}
                    renderNode={htmlNodeRendering}
                />
                <TouchableOpacity
                    onPress={() => visitWebsiteVersion(props.postLink)}
                    style={{...inContentButton}}
                >
                    <Text style={{fontSize: 15, color: gray100}}>Read Website Version</Text>
                </TouchableOpacity>
            </ScrollView>
        </View>
    )
}

PostView.propTypes = {
    postID: PropTypes.number.isRequired,
    title: PropTypes.string.isRequired,
    excerpt: PropTypes.string,
    content: PropTypes.string.isRequired,
    postDate: PropTypes.string.isRequired,
    author: PropTypes.object.isRequired,
    postType: PropTypes.string.isRequired,
    subscription: PropTypes.string,
    postLink: PropTypes.string.isRequired,
    actionsToTake: PropTypes.oneOfType([
        PropTypes.bool,
        PropTypes.array
    ]).isRequired
}
PostView.defaultProps = {
    actionsToTake: false
}

const styles = StyleSheet.create({
    postContainer: {

    },
    headerTopRow: {
        flexDirection: 'row',
        justifyContent: 'space-between'
    },
    headerBottomRow: {
        flexDirection: 'row',
        marginTop: 15,
        backgroundColor: postHeaderGray,
        marginBottom: -16,
        marginRight: -15,
        marginLeft: -15,
        padding: 15,
        borderColor: gray200,
        borderTopWidth: 2
    },
    headerContainer: {
        borderBottomWidth: 1,
        borderColor: gray100,
        shadowColor: '#000',
        backgroundColor: gray100,
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.8,
        shadowRadius: 2,
        elevation: 1,
        padding: 15

    },
    scrollContainer: {
        ...htmlContentScrollContainer
    },
    iconStyles: {
        alignSelf: 'flex-start'
    },
    postTitle: {
        ...PostTitle,
        fontSize: 25
    },
    postType: {
        ...MetaTitle,
        marginLeft: 5
    },
    postDate: {
        alignSelf: 'flex-end',
        ...MetaTitle
    },
    authorName: {
        ...MetaTitle,
        flex: 1,
        flexWrap: 'wrap'
    },
    authorImage: {
        width: 75,
        height: 75,
        borderRadius: 35
    },
    subscriptionTitle: {
        ...MetaTitle,
        flexWrap: 'wrap',
        fontWeight: '700'
    },
    // This isn't the best solution but I think it will work for now
    contentContainer: {
        // marginBottom: 600
    }
})

export default PostView

Solution

  • Ok I figured it out! They key was to use the stickyHeaderIndices property for ScrollView, this then takes into account everything and I am able to scroll all the way to the bottom. Below is the code that worked:

    const visitWebsiteVersion = (url) => {
        OutOfAppLinking(url)
    }
    const PostView = (props) => {
        let postTypeName;
        let postIcon;
        switch (props.postType) {
            case 'something1':
                postTypeName = 'Something1'
                postIcon = <FontAwesome name={'bell'} size={20} color={gray500} style={styles.iconStyles}/>
                break;
            case 'something2':
                postTypeName = 'Something2'
                postIcon = <FontAwesome name={'newspaper-o'} size={15} color={gray500} style={styles.iconStyles}/>
                break;
            case 'something3':
                postTypeName = 'Something3'
                postIcon = <FontAwesome name={'book'} size={15} color={gray500} style={styles.iconStyles}/>
                break;
            case 'something4':
                postTypeName = 'Something4'
                postIcon = <FontAwesome name={'book'} size={15} color={gray500} style={styles.iconStyles}/>
                break;
            default:
                postTypeName = ''
                postIcon = ''
        }
    
        const {actionsToTake, content, postDate, title, author, subscription, postLink} = props
        return(
            <View style={styles.postContainer}>
    
                <ScrollView
                    style={styles.scrollContainer}
                    showsVerticalScrollIndicator={false}
                    stickyHeaderIndices={[0]}
                >
                    <View style={styles.headerContainer}>
                        <View style={styles.headerTopRow}>
                            <View style={{flexDirection: 'row'}}>
                                {postIcon}
                                <Text style={styles.postType}>{postTypeName}</Text>
                            </View>
                            <Text style={styles.postDate}>{monthDayYearConversion(postDate)}</Text>
                        </View>
                        <Text style={styles.postTitle}>{title}</Text>
                        <View style={styles.headerBottomRow}>
                            <View>
                                <View style={{flexDirection: 'row'}}>
                                    <Text style={styles.authorName}>By {author.name}</Text>
                                </View>
                                <View style={{flexDirection: 'row'}}>
                                    <Text style={styles.subscriptionTitle}>{subscription}</Text>
                                </View>
                            </View>
                        </View>
                    </View>
                    <View style={{...htmlContentScrollContainer}}>
                        {actionsToTake ?
                            <ActionsToTake actions={actionsToTake}/>
                        :null}
                        <HTMLView
                            value={content}
                            stylesheet={htmlContentStylesheet}
                            style={styles.contentContainer}
                            renderNode={htmlNodeRendering}
                        />
                        <TouchableOpacity
                            onPress={() => visitWebsiteVersion(postLink)}
                            style={{...inContentButton}}
                        >
                            <Text style={{fontSize: 15, color: gray100}}>Read Website Version</Text>
                        </TouchableOpacity>
                    </View>
                </ScrollView>
            </View>
        )
    }
    
    PostView.propTypes = {
        postID: PropTypes.number.isRequired,
        title: PropTypes.string.isRequired,
        excerpt: PropTypes.string,
        content: PropTypes.string.isRequired,
        postDate: PropTypes.string.isRequired,
        author: PropTypes.object.isRequired,
        postType: PropTypes.string.isRequired,
        subscription: PropTypes.string,
        postLink: PropTypes.string.isRequired,
        actionsToTake: PropTypes.oneOfType([
            PropTypes.bool,
            PropTypes.array
        ]).isRequired
    }
    PostView.defaultProps = {
        actionsToTake: false
    }
    
    const styles = StyleSheet.create({
        postContainer: {
    
        },
        headerTopRow: {
            flexDirection: 'row',
            justifyContent: 'space-between'
        },
        headerBottomRow: {
            flexDirection: 'row',
            marginTop: 15,
            backgroundColor: postHeaderGray,
            marginBottom: -16,
            marginRight: -15,
            marginLeft: -15,
            padding: 15,
            borderColor: gray200,
            borderTopWidth: 2
        },
        headerContainer: {
            borderBottomWidth: 1,
            borderColor: gray100,
            shadowColor: '#000',
            backgroundColor: gray100,
            shadowOffset: { width: 0, height: 2 },
            shadowOpacity: 0.8,
            shadowRadius: 2,
            elevation: 1,
            padding: 15,
            marginBottom: 15
        },
        scrollContainer: {
    
        },
        iconStyles: {
            alignSelf: 'flex-start'
        },
        postTitle: {
            ...PostTitle,
            fontSize: 25
        },
        postType: {
            ...MetaTitle,
            marginLeft: 5
        },
        postDate: {
            alignSelf: 'flex-end',
            ...MetaTitle
        },
        authorName: {
            ...MetaTitle,
            flex: 1,
            flexWrap: 'wrap'
        },
        authorImage: {
            width: 75,
            height: 75,
            borderRadius: 35
        },
        subscriptionTitle: {
            ...MetaTitle,
            flexWrap: 'wrap',
            fontWeight: '700'
        },
        // This isn't the best solution but I think it will work for now
        contentContainer: {
            // marginBottom: 600
        }
    })
    
    export default PostView