上一节React Native带你实现scrollable-tab-view(三)中我们最后实现了我们scrollable-tab-view的效果为:
还记得我们上一节最后留下的问题吗?比如我们有很多个页面,然后一出来就加载那么多页面的话,再牛掰的手机都扛不住,我们想做的是:
1、第一次加载页面的时候,假设有三个页面,第一次就只加载第一个页面,(用户也可以选择预加载出第二个和第三个页面)。
2、每次滑动的时候,滑动到某个页面,只渲染加载过的页面跟需要预加载的页面,其它的页面先用一个空白页面替代。
哈哈~ 说了那么多文字性的东西,小伙伴是否已经疲惫了呢?下面我们直接撸代码了。
我们把每一个加载过的和需要加载的页面用一个对应的集合sceneKeys对应起来:
export default class ScrollableTab extends Component {
static propTypes = {
prerenderingSiblingsNumber: PropTypes.number,//预加载的页面
}
static defaultProps = {
prerenderingSiblingsNumber: 0,//不需要预加载
}
// 构造
constructor(props) {
super(props);
// 初始状态
this.state = {
containerWidth: screenW,
currentPage: 0,//当前页面
scrollXAnim: new Animated.Value(0),
scrollValue: new Animated.Value(0),
sceneKeys: this._newSceneKeys({currentPage: 0}),
};
}
......
/**
* 生成需要渲染的页面跟渲染过的页面的集合
* @param previousKeys 之前的集合
* @param currentPage 当前页面
* @param children 子控件
* @private
*/
_newSceneKeys({previousKeys = [], currentPage = 0, children = this.props.children,}) {
let newKeys = [];
this._children().forEach((child, index)=> {
const key = this._makeSceneKey(child, index);
//页面是否渲染过||是否需要预加载
if (this._keyExists(previousKeys, key) || this._shouldSceneRender(index, currentPage)) {
newKeys.push(key);
}
});
return newKeys;
}
/**
* 生成唯一key
* @param child 子控件
* @param index 下标
* @private
*/
_makeSceneKey(child, index) {
return (child.props.tabLabel + '_' + index);
}
/**
* 判断key是否存在
* @param previousKeys key集合
* @param key 当前key
* @private
*/
_keyExists(previousKeys, key) {
return (previousKeys.find((sceneKey)=>sceneKey === key));
}
/**
* 是否需要预加载
* @private
*/
_shouldSceneRender(index, currentPage) {
const siblingsNumber = this.props.prerenderingSiblingsNumber;
//比如当前页面为1,预加载1个,也就是我们需要显示0、1、2三个页面,所[-1<x<3]
return (index < (currentPage + siblingsNumber + 1) && index > (currentPage - siblingsNumber - 1));
}
然后我们渲染子控件的时候,在我们集合中的控件我们就渲染出来,不在集合中的控件我们直接用一个空view替代(因为我们需要滑动操作)。
/**
* 渲染主体内容
* @private
*/
_renderScrollableContent() {
return (
<Animated.ScrollView
ref={(ref) => {
this._scrollView = ref;
}}
....
>
{this._renderContentView()}
</Animated.ScrollView>
);
}
/**
* 渲染子view
* @private
*/
_renderContentView() {
let scenes = [];
this._children().forEach((child, index)=> {
const sceneKey = this._makeSceneKey(child, index);
let scene = null;
if (this._keyExists(this.state.sceneKeys, sceneKey)) {
scene = (child);
} else {
scene = (<View tabLabel={child.tabLabel}/>);
}
scenes.push(
<View
key={child.key}
style={{width: this.state.containerWidth}}
>
{scene}
</View>
);
});
return scenes;
}
然后我们在滑动开始和结束的时候,需要重新更新下ke yScenes,也就是重新计算我们需要渲染哪些页面:
/**
* scrollview开始跟结束滑动回调
* @param e
* @private
*/
_onMomentumScrollBeginAndEnd = (e) => {
let offsetX = e.nativeEvent.contentOffset.x;
let page = Math.round(offsetX / this.state.containerWidth);
if (this.state.currentPage !== page) {
this._updateKeyScenes(page);
}
}
/**
* 更新sceneskey和当前页面
* @param nextPage
* @private
*/
_updateKeyScenes(nextPage) {
let sceneKeys = this._newSceneKeys({previousKeys: this.state.sceneKeys, currentPage: nextPage})
this.setState({
currentPage: nextPage,
sceneKeys: sceneKeys,
});
好啦~~ 我们走一遍代码:
可以看到,我们页面刚渲染的时候然后滑动到第二页的时候会闪烁一下,那是因为我们加载第一个页面的时候,第二页并没有加载,而是在滑动到第二页的时候加载完毕的,所以当我们再滑回到第一第二页的时候,页面里面出来了,(这也是我们一开始说的“滑到哪加载到哪,页面加载完毕后就不需要重新加载了”)那么问题来了,说好的预加载呢? 哈哈~~ 有的!! 我们把prerenderingSiblingsNumber改为1,也就是说一进来就加载第一页跟第二页,我们再看看效果:
可以看到,当我们滑动到第二页的时候页面立马出来了,然后第二页滑动到第三页的时候页面也是立马出来了。
好啦~~ 当我们页面滑动完毕后我们有改变我们的ke y Scenes集合,但是我们点击tabview的时候是不是也得改变下我们的ke y Scenes集合呢? 我们没改变,所以我们点击一下tab切换一下:
可以看到,我们点击第一页跟第二页都有页面,然后点到第三页就空白了,这是为什么呢?? 哈哈~ 因为我们前面有设置一个预加载,所以第二页跟第一页一开始就渲染出来了,但是当我们切换到第三页的时候我们的ke yScenes还是之前的集合,所以出来的还是两个页面,所以我们在点击tab回调的地方也得重新计算下ke yScenes集合:
/**
* 渲染tabview
* @private
*/
_renderTabView() {
let tabParams = {
tabs: this._children().map((child)=>child.props.tabLabel),
activeTab: this.state.currentPage,
scrollValue: this.state.scrollValue,
containerWidth: this.state.containerWidth,
};
return (
<DefaultTabBar
{...tabParams}
style={[{width: this.state.containerWidth}]}
onTabClick={(page)=>this.goToPage(page)}
/>
);
}
/**
* 滑动到指定位置
* @param pageNum page下标
* @param scrollAnimation 是否需要动画
*/
goToPage(pageNum, scrollAnimation = true) {
if (this._scrollView && this._scrollView._component && this._scrollView._component.scrollTo) {
this._scrollView._component.scrollTo({x: pageNum * this.state.containerWidth, scrollAnimation});
this._updateKeyScenes(pageNum);
}
}
然后我们把预加载去掉:
static propTypes = {
prerenderingSiblingsNumber: PropTypes.number,//预加载的页面
}
static defaultProps = {
prerenderingSiblingsNumber: 0,//不需要预加载
}
我们再次运行代码:
可以看到,我们完美的实现了我们的需求!
最后附上本节代码:
DefaultTabBar.js:
/**
* @author YASIN
* @version [React-Native Pactera V01, 2017/9/5]
* @date 17/2/23
* @description DefaultTabBar
*/
import React, {
Component, PropTypes,
} from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
Animated,
} from 'react-native';
const screenW = Dimensions.get('window').width;
const screenH = Dimensions.get('window').height;
export default class DefaultTabBar extends Component {
static propTypes = {
tabs: PropTypes.array,
activeTab: PropTypes.number,//当前选中的tab
style: View.propTypes.style,
onTabClick: PropTypes.func,
containerWidth: PropTypes.number,
}
// 构造
constructor(props) {
super(props);
// 初始状态
this.state = {};
}
render() {
let {containerWidth, tabs, scrollValue}=this.props;
//给传过来的动画一个插值器
const left = scrollValue.interpolate({
inputRange: [0, 1,], outputRange: [0, containerWidth / tabs.length,],
});
let tabStyle = {
width: containerWidth / tabs.length,
position: 'absolute',
bottom: 0,
left,
}
return (
<View style={[styles.container, this.props.style]}>
{this.props.tabs.map((name, page) => {
const isTabActive = this.props.activeTab === page;
return this._renderTab(name, page, isTabActive);
})}
<Animated.View
style={[styles.tabLineStyle, tabStyle]}
/>
</View>
);
}
/**
* 渲染tab
* @param name 名字
* @param page 下标
* @param isTabActive 是否是选中的tab
* @private
*/
_renderTab(name, page, isTabActive) {
let tabTextStyle = null;
//如果被选中的style
if (isTabActive) {
tabTextStyle = {
color: 'green'
};
} else {
tabTextStyle = {
color: 'red'
};
}
let self = this;
return (
<TouchableOpacity
key={name + page}
style={[styles.tabStyle]}
onPress={()=>this.props.onTabClick(page)}
>
<Text style={[tabTextStyle]}>{name}</Text>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
container: {
width: screenW,
flexDirection: 'row',
alignItems: 'center',
height: 50,
},
tabStyle: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
tabLineStyle: {
height: 2,
backgroundColor: 'navy',
}
});
ScrollableTab.js:
/**
* @author YASIN
* @version [React-Native Pactera V01, 2017/9/5]
* @date 2017/9/5
* @description index
*/
import React, {
Component, PropTypes,
} from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Dimensions,
TouchableOpacity,
Animated,
} from 'react-native';
const screenW = Dimensions.get('window').width;
const screenH = Dimensions.get('window').height;
import DefaultTabBar from './DefaultTabBar';
export default class ScrollableTab extends Component {
static propTypes = {
prerenderingSiblingsNumber: PropTypes.number,//预加载的页面
}
static defaultProps = {
prerenderingSiblingsNumber: 0,//不需要预加载
}
// 构造
constructor(props) {
super(props);
// 初始状态
this.state = {
containerWidth: screenW,
currentPage: 0,//当前页面
scrollXAnim: new Animated.Value(0),
scrollValue: new Animated.Value(0),
sceneKeys: this._newSceneKeys({currentPage: 0}),
};
}
render() {
return (
<View
style={styles.container}
onLayout={this._onLayout}
>
{/*渲染tabview*/}
{this._renderTabView()}
{/*渲染主体内容*/}
{this._renderScrollableContent()}
</View>
);
}
componentDidMount() {
//设置scroll动画监听
this.state.scrollXAnim.addListener(({value})=> {
let offset = value / this.state.containerWidth;
this.state.scrollValue.setValue(offset);
});
}
componentWillUnMount() {
//移除动画监听
this.state.scrollXAnim.removeAllListeners();
this.state.scrollValue.removeAllListeners();
}
/**
* 渲染tabview
* @private
*/
_renderTabView() {
let tabParams = {
tabs: this._children().map((child)=>child.props.tabLabel),
activeTab: this.state.currentPage,
scrollValue: this.state.scrollValue,
containerWidth: this.state.containerWidth,
};
return (
<DefaultTabBar
{...tabParams}
style={[{width: this.state.containerWidth}]}
onTabClick={(page)=>this.goToPage(page)}
/>
);
}
/**
* 渲染主体内容
* @private
*/
_renderScrollableContent() {
return (
<Animated.ScrollView
ref={(ref) => {
this._scrollView = ref;
}}
style={{width: this.state.containerWidth}}
pagingEnabled={true}
horizontal={true}
onMomentumScrollBegin={this._onMomentumScrollBeginAndEnd}
onMomentumScrollEnd={this._onMomentumScrollBeginAndEnd}
scrollEventThrottle={15}
onScroll={Animated.event([{
nativeEvent: {contentOffset: {x: this.state.scrollXAnim}}
}], {
useNativeDriver: true,
})}
bounces={false}
scrollsToTop={false}
>
{this._renderContentView()}
</Animated.ScrollView>
);
}
/**
* 渲染子view
* @private
*/
_renderContentView() {
let scenes = [];
this._children().forEach((child, index)=> {
const sceneKey = this._makeSceneKey(child, index);
let scene = null;
if (this._keyExists(this.state.sceneKeys, sceneKey)) {
scene = (child);
} else {
scene = (<View tabLabel={child.tabLabel}/>);
}
scenes.push(
<View
key={child.key}
style={{width: this.state.containerWidth}}
>
{scene}
</View>
);
});
return scenes;
}
/**
* 获取子控件数组集合
* @param children
* @returns {*}
* @private
*/
_children(children = this.props.children) {
return React.Children.map(children, (child)=>child);
}
/**
* 获取控件宽度
* @param e
* @private
*/
_onLayout = (e)=> {
let {width}=e.nativeEvent.layout;
if (this.state.containerWidth !== width) {
this.setState({
containerWidth: width,
});
}
}
/**
* scrollview开始跟结束滑动回调
* @param e
* @private
*/
_onMomentumScrollBeginAndEnd = (e) => {
let offsetX = e.nativeEvent.contentOffset.x;
let page = Math.round(offsetX / this.state.containerWidth);
if (this.state.currentPage !== page) {
this._updateKeyScenes(page);
}
}
/**
* 更新sceneskey和当前页面
* @param nextPage
* @private
*/
_updateKeyScenes(nextPage) {
let sceneKeys = this._newSceneKeys({previousKeys: this.state.sceneKeys, currentPage: nextPage})
this.setState({
currentPage: nextPage,
sceneKeys: sceneKeys,
});
}
/**
* 滑动到指定位置
* @param pageNum page下标
* @param scrollAnimation 是否需要动画
*/
goToPage(pageNum, scrollAnimation = true) {
if (this._scrollView && this._scrollView._component && this._scrollView._component.scrollTo) {
this._scrollView._component.scrollTo({x: pageNum * this.state.containerWidth, scrollAnimation});
this._updateKeyScenes(pageNum);
}
}
/**
* 生成需要渲染的页面跟渲染过的页面的集合
* @param previousKeys 之前的集合
* @param currentPage 当前页面
* @param children 子控件
* @private
*/
_newSceneKeys({previousKeys = [], currentPage = 0, children = this.props.children,}) {
let newKeys = [];
this._children().forEach((child, index)=> {
const key = this._makeSceneKey(child, index);
//页面是否渲染过||是否需要预加载
if (this._keyExists(previousKeys, key) || this._shouldSceneRender(index, currentPage)) {
newKeys.push(key);
}
});
return newKeys;
}
/**
* 生成唯一key
* @param child 子控件
* @param index 下标
* @private
*/
_makeSceneKey(child, index) {
return (child.props.tabLabel + '_' + index);
}
/**
* 判断key是否存在
* @param previousKeys key集合
* @param key 当前key
* @private
*/
_keyExists(previousKeys, key) {
return (previousKeys.find((sceneKey)=>sceneKey === key));
}
/**
* 是否需要预加载
* @private
*/
_shouldSceneRender(index, currentPage) {
const siblingsNumber = this.props.prerenderingSiblingsNumber;
//比如当前页面为1,预加载1个,也就是我们需要显示0、1、2三个页面,所[-1<x<3]
return (index < (currentPage + siblingsNumber + 1) && index > (currentPage - siblingsNumber - 1));
}
}
const styles = StyleSheet.create({
container: {
width: screenW,
flex: 1,
marginTop: 22,
},
});
下一节我们去实现一下ScrollableTabBar.js,也就是说上面的tabview需要映射底部的scrollview滑动,让tabview跟随底部滑动而滑动,小伙伴可以先思考思考哦。
欢迎入群,欢迎交流,大牛勿喷,下一节见!!