March 07, 2020
import React, { useContext, useState, useEffect } from 'react'
import { FlatList } from 'react-native'
import {
NavigationScreenProp,
NavigationState,
NavigationProp,
} from 'react-navigation'
import { RandomUserDataContext } from '~/Context/RandomUserData'
import IconButton from '~/Components/IconButton'
import Feed from '~/Components/Feed'
import StoryList from './StoryList'
interface Props {
navigation: NavigationProp<NavigationState>
}
const MyFeed = ({ navigation }: Props) => {
const { getMyFeed } = useContext(RandomUserDataContext)
const [feedList, setFeedList] = useState<Array<IFeed>>([])
const [storyList, setStoryList] = useState<Array<IFeed>>([])
const [loading, setLoading] = useState<boolean>(false)
useEffect(() => {
setFeedList(getMyFeed())
setStoryList(getMyFeed())
}, [])
return (
<FlatList
data={feedList}
keyExtractor={(item, index) => {
return `myfeed-${index}`
}}
showsVerticalScrollIndicator={false}
onRefresh={() => {
setLoading(true)
setTimeout(() => {
setFeedList(getMyFeed())
setStoryList(getMyFeed())
setLoading(false)
}, 2000)
}}
onEndReached={() => {
setFeedList([...feedList, ...getMyFeed()])
}}
onEndReachedThreshold={0.5}
refreshing={loading}
ListHeaderComponent={<StoryList storyList={storyList} />}
renderItem={({ item, index }) => (
<Feed
id={index}
name={item.name}
photo={item.photo}
description={item.description}
images={item.images}
/>
)}
/>
)
}
MyFeed.navigationOptions = {
title: 'SNS App',
headerLeft: <IconButton iconName="camera" />,
headerRight: (
<>
<IconButton iconName="live" />
<IconButton iconName="send" />
</>
),
}
export default MyFeed
화면에 헤더에 표시될 스토리리스트와 피드 리스트를 상태관리하여 만들어 사용했다. MyFeed컴포넌트가 화면에 표시되면 useContext의 getMyFeed함수를 사용하여 데이터를 가져와 state값을 설정하였다.
FlatList의 onRefresh를 사용하여 당겨서 새로고침을 구현하고 이때 헤더의 스토리리스트와 피드리스트를 다시 가져와 화면을 갱신한다. onEndReached를 사용하여 스크롤이 최하단일때 데이터를 다시 가져와 추가한다.
interface Props {
id: number
images: Array<string>
}
const FeedBody = ({ id, images }: Props) => {
const [indicatorIndex, setIndicatorIndex] = useState<number>(0)
const imageLength = images.length
return (
<Container>
<ScrollView
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
scrollEnabled={imageLength > 1}
onScroll={(event: NativeSyntheticEvent<NativeScrollEvent>) => {
setIndicatorIndex(
event.nativeEvent.contentOffset.x / Dimensions.get('window').width
)
}}
>
{images.map((image, index) => (
<ImageContainer key={`FeedImage-${index}`}>
<Image
source={{ uri: image as string }}
style={{ width: Dimensions.get('window').width, height: 400 }}
/>
</ImageContainer>
))}
</ScrollView>
<FeedMenuContainer>
<MenuContainer>
<IconButton iconName="favorite" />
<IconButton iconName="comment" />
<IconButton iconName="send" />
</MenuContainer>
<MenuContainer>
<FeedImageIndicatorContainer>
{imageLength > 1 &&
images.map((image, index) => (
<FeedImageIndicator
key={`FeedImageIndicator-${index}`}
style={{
backgroundColor:
indicatorIndex >= index && indicatorIndex < index + 1
? '#3796Ef'
: '#d3d3d3',
}}
/>
))}
</FeedImageIndicatorContainer>
</MenuContainer>
<MenuContainer style={{ justifyContent: 'flex-end' }}>
<IconButton iconName="bookmark" />
</MenuContainer>
</FeedMenuContainer>
</Container>
)
}
export default FeedBody
이미지 리스트를 페이지 형식으로 스크롤하기 위해 pagingEnabled를 true로 설정하였다. 또한 직접 제작한 인디케이터를 사용하기 위해, showsHorizontalScrollIndicator를 false를 설정함으로써 ScrollView가 기본적으로 제공하는 인디케이터를 비활성화 하였다.
사용자에 의해 스크롤 이벤트가 발생하면(onScroll) 그렇게 구한 스크롤의 위치를 useState를 사용하여 생성한 setIndicatorIndex 함수를 통해 설정하였다.
IndicatorIndex를 사용하여 화면에 표시된 인디케이터의 색상을 변경하여 사용자에게 현재 이미지의 위치가 이미지 리스트에서 어디에 해당하는지 알 수 있다.
import React, { useContext, useState, useEffect, createRef } from 'react'
import {
Dimensions,
NativeSyntheticEvent,
NativeScrollEvent,
ScrollView,
} from 'react-native'
import styled from 'styled-components/native'
import { RandomUserDataContext } from '~/Context/RandomUserData'
import Tab from '~/Screen/Tab'
import NotificationList from './NotificationList'
const ProfileTabContainer = styled.View`
flex-direction: row;
background-color: #feffff;
`
const Label = styled.Text`
color: #929292;
text-align: center;
`
const TabContainer = styled.SafeAreaView`
width: 100%;
height: ${Dimensions.get('window').height}px;
`
interface Props {}
const Notification = ({}: Props) => {
const { getMyFeed } = useContext(RandomUserDataContext)
const [followingList, setFollowingList] = useState<Array<IFeed>>([])
const [myNotifications, setMyNotifications] = useState<Array<IFeed>>([])
const [tabIndex, setTabIndex] = useState<number>(1)
const width = Dimensions.get('window').width
const tabs = ['팔로잉', '내 소식']
const refScrollView = createRef<ScrollView>()
useEffect(() => {
setFollowingList(getMyFeed(24))
setMyNotifications(getMyFeed(24))
}, [])
return (
<TabContainer>
<ProfileTabContainer>
{tabs.map((label: string, index: number) => (
<Tab
key={`tabs-${index}`}
selected={tabIndex === index}
label={label}
onPress={() => {
setTabIndex(index)
const node = refScrollView.current
if (node) {
node.scrollTo({ x: width * index, y: 0, animated: true })
}
}}
/>
))}
</ProfileTabContainer>
<ScrollView
ref={refScrollView}
horizontal={true}
showsHorizontalScrollIndicator={false}
pagingEnabled={true}
stickyHeaderIndices={[0]}
onScroll={(event: NativeSyntheticEvent<NativeScrollEvent>) => {
const index = event.nativeEvent.contentOffset.x / width
setTabIndex(index)
}}
contentOffset={{ x: width, y: 0 }}
>
<NotificationList
id={0}
width={width}
data={followingList}
onEndReached={() => {
setFollowingList([...followingList, ...getMyFeed(24)])
}}
/>
<NotificationList
id={1}
width={width}
data={myNotifications}
onEndReached={() => {
setMyNotifications([...myNotifications, ...getMyFeed(24)])
}}
/>
</ScrollView>
</TabContainer>
)
}
export default Notification
Notification 컴포넌트는 Tab컴포넌트를 활용하여 탭으로 구성되어 있으며 state로 현재 선택되어 있는 탭을 구분할 수 있도록 설정하였다. 다른 컴포넌트들과는 다르게 createRef를 사용하였으며, createRef는 리액트에서 컴포넌트를 컴포넌트 외부에서 직접 컨트롤하여 컴포넌트의 이벤트 또는 함수를 다룰 때 사용한다. 이번 예제에서 탭을 선택하였을 때, ScrollView를 직접 컨트롤하여 해당하는 화면을 표시하도록 설정하였다.
import React, { useState, useContext, useEffect } from 'react'
import {
NativeScrollEvent,
Image,
Dimensions,
NativeSyntheticEvent,
ScrollView,
ImageSourcePropType,
} from 'react-native'
import { NavigationScreenProp, NavigationState } from 'react-navigation'
import styled from 'styled-components/native'
import { RandomUserDataContext } from '~/Context/RandomUserData'
import IconButton from '~/Components/IconButton'
import Tab from '~/Screen/Tab'
import ProfileHeader from './ProfileHeader'
import ProfileBody from './ProfileBody'
interface Props {
navigation: NavigationScreenProp<NavigationState>
}
const Profile = ({ navigation }: Props) => {
const { getMyFeed } = useContext(RandomUserDataContext)
const [feedList, setFeedList] = useState<Array<IFeed>>([])
const imageWidth = Dimensions.get('window').width / 3
const tabs = [
require('~/Assets/Images/ic_grid_image_focus.png'),
require('~/Assets/Images/ic_tab_image.png'),
]
useEffect(() => {
setFeedList(getMyFeed(24))
}, [])
const isBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}: NativeScrollEvent) => {
return layoutMeasurement.height + contentOffset.y >= contentSize.height
}
return (
<ScrollView
stickyHeaderIndices={[2]}
onScroll={(event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (isBottom(event.nativeEvent)) {
setFeedList([...feedList, ...getMyFeed(24)])
}
}}
>
<ProfileHeader
image="http://api.randomuser.me/portraits/women/68.jpg"
posts={3431}
follower={6530}
following={217}
/>
<ProfileBody
name="Sara Lambert"
description="On Friday, April 14, being Good-Friday, i repaired to him in the\nmorning, according to my usual custom on that day, and breakfasted\nwith him"
/>
<ProfileTabContainer>
{tabs.map((image: ImageSourcePropType, index: number) => (
<Tab
key={`tab-${index}`}
selected={index === 0}
imageSource={image}
/>
))}
<FeedContainer>
{feedList.map((feed: IFeed, index: number) => (
<ImageContainer
key={`feed-list-${index}`}
style={{
paddingLeft: index % 3 === 0 ? 0 : 1,
paddingRight: index % 3 === 2 ? 0 : 1,
width: imageWidth,
}}
>
<Image
source={{ uri: feed.images[0] }}
style={{ width: imageWidth, height: imageWidth }}
/>
</ImageContainer>
))}
</FeedContainer>
</ProfileTabContainer>
</ScrollView>
)
}
interface INaviProps {
navigation: NavigationScreenProp<NavigationState>
}
Profile.navigationOptions = ({ navigation }: INaviProps) => {
return {
title: 'profile',
headerRight: <IconButton iconName="menu" onPress={navigation.openDrawer} />,
}
}
export default Profile
화면을 스크롤하다 보면, 중간에 있는 탭이 상단에 고정되는데 ScrollView의 stickyHeaderIndices를 사용하면 된다. 에러가 조금있어서 체크를 해봐야 할것같다. stickyHeaderIndices는 상단에 고정하고 싶은 리스트 아이템의 인덱스를 리스트 형태로 지정해 주면 해당 아이템이 상단 부분에 도착하였을 때 고정되도록 설정할 수 있다. ScrollView 컴포넌트는 FlatList 컴포넌트와 다르게 onEndReached를 Props로 가지고 있지 않다. 따라서 무한 스크롤을 onScroll 이벤트를 활용하여 구현했다.