效果图
在实现这个需求的时候,我的第一想法就是看看官网有没有类似的组件,然后我找到了TabNavigator,官方文档介绍 使用这个组件,需要事先导入react-navigation包.TabNavigator是Navigation库中提供的3个组件中的其中一个,不仅可以实现顶部导航,也还可以实现底部导航,另外2个组件分别是StackNavigator和DrawerNavigator,前者是实现页面栈的导航,比如当前页面和下级页面的切换,后者是实现侧滑菜单导航的.
但是,TabNavigator虽好,我却没有找到有什么办法可以动态添加场景的方式,文档也没有给出说明,这就很尴尬啊.因为我这个需求的顶部tab数量和内容都是根据接口字段返回确定的,而TabNavigator都是事先写死的几个tab名称和对应场景.
既然官方的库没办法实现,我就随便搜索下别人写好的第三方库,找到一个react_native_scrollable_tab_view库,可以满足我这个需求,对于android平台,该库底层其实是用ViewPager来实现页面切换效果的,然而我在使用的时候发现当我的页面是ListView组件的时候,会出现从其他页面切换回来的时候当前页面的数据会消失,而且没有请求接口,查看了下代码发现ListView的randerRow方法的rowData既然是空的,这就很神奇了,发现控制台输出的log是gc在回收.到底是什么鬼原因,我也没找到.
没办法,最后还是得自己写一个类似的组件,下面就说说我这个自定义的组件实现方式吧,整个页面分为上下2部分,都是用ScrollView来实现的.
顶部tab导航单独封装了一个组件叫TabIndicator,默认最大显示5个tab,超出支持滑动显示,指示器用了Animated来实现水平方向的移动动画,tab选中和未选中的color,是根据当前点击的index与tab属性的index作比较,如果相同表示选中的是当前tab,然后通过setState来修改color,比较难的地方就是实现点击屏幕左右边界时自动滚出上一个或者下一个tab效果了,这里用到了ScrollView的scrollTo方法:
scrollTo(y: number | { x?: number, y?: number, animated?: boolean }, x: number, animated: boolean)
滚动到指定的x, y偏移处。第三个参数为是否启用平滑滚动动画。
使用示例:
scrollTo({x: 0, y: 0, animated: true})
剩下的工作就是找出x和当前index的变化规律了,这个要多测试才好理解,我也是弄了好久才找到规律的.
ok,下面贴出TabIndicator的详细代码
import React, { Component } from 'react';
import {
View,
Text,
ScrollView,
StyleSheet,
Dimensions,
TouchableHighlight,
Animated,
} from 'react-native';
const screenWidth = Dimensions.get('window').width;
const defaultVisiableCount = 5; //默认显示的tab数量
export default class TabIndicator extends Component {
static propTypes = {
tabBarActiveTextColor: React.PropTypes.string,//tab选中时显示的颜色
tabBarInactiveTextColor: React.PropTypes.string,//tab未选中时的颜色
tabNames: React.PropTypes.array,//tab名称集合
tabTextStyle: React.PropTypes.any,//tab文本的样式
tabBarUnderlineStyle: React.PropTypes.any,//tab下划线的样式
tabHeight: React.PropTypes.number,//tab的高度
};
static defaultProps = {
tabBarActiveTextColor: '#686868',
tabBarInactiveTextColor: '#cccccc',
tabNames: [],
tabHeight: 50,
}
constructor(props) {
super(props)
this.state = {
transAnimX: new Animated.Value(0), //指示器平移动画
};
this.tabManager = [];
this.tabRef = [];
this.tabWidth = 0;//单个tab的宽度
this.currPosition = 0;//当前位置
this.totalTabWidth = 0;//所有tab宽度之和
if (this.props.tabNames) {
//计算单个tab的宽度
this.tabWidth = this.props.tabNames.length > defaultVisiableCount ?
screenWidth / defaultVisiableCount :
screenWidth / this.props.tabNames.length;
this.totalTabWidth = this.tabWidth * this.props.tabNames.length;
//创建所有tabView
for (let index = 0; index < this.props.tabNames.length; index++) {
this.tabManager.push(
<Tab ref={(r) => this.tabRef[index] = r}
key={index}
index={index}
title={this.props.tabNames[index]}
onTabPress={(index) => {
this.clickToTrans(index);
}}
tabWidth={this.tabWidth}
tabHeight={this.props.tabHeight}
style={{ ...this.props.tabTextStyle }}
selectedTextColor={this.props.tabBarActiveTextColor}
unselectedTextColor={this.props.tabBarInactiveTextColor}
/>
)
}
}
}
//点击tab时触发
clickToTrans = (index) => {
this.currPosition = index;
if (this.props.onTabPress) {
this.props.onTabPress(index);//通知外部点击的位置
} else {
this.swipeToTrans(index) //外部没有设置关联时自己处理
}
}
//关联的ScrollView滚动时触发
swipeToTrans = (index) => {
this.currPosition = index;
console.log("swipeToTrans:index=" + index)
if (index >= (defaultVisiableCount - 1)) {
this.scrollToNext()
} else if (index >= 0) {
console.log(index - (defaultVisiableCount - 1) > 0)
this.scrollToPre()
}
//移动指示器
this.state.transAnimX.setValue(index)
//遍历所有tab,根据当前选中的index修改颜色
this.tabRef.map((ref, key) => { ref.changeState(index) });
}
//滚动下一个tab
scrollToNext = () => {
console.log("scrollToNext")
let x = (this.currPosition - (defaultVisiableCount - 1) + 1) * this.tabWidth
this.sv.scrollTo({ x: x, y: 0, animated: true })
}
//滚动上一个tab
scrollToPre = () => {
console.log("scrollToPre")
let x = - this.tabWidth
this.sv.scrollTo({ x: x, y: 0, animated: true })
}
render() {
return (
<View style={{ height: this.props.tabHeight, width: screenWidth, backgroundColor: 'white' }}>
<ScrollView
horizontal={true}
keyboardDismissMode='on-drag'
showsHorizontalScrollIndicator={false}
overScrollMode='never'
ref={(r) => this.sv = r}
>
{/* 所有tab */}
{this.tabManager}
{/* 指示器 */}
<Animated.View style={[styles.tabLine, { width: this.tabWidth },
{
transform: [
{
translateX: this.state.transAnimX.interpolate({
inputRange: [0, this.props.tabNames.length], //0~tab个数
outputRange: [0, this.totalTabWidth] //0~所有tab的总宽度
})
}
]
}
]} />
</ScrollView>
{/* 默认线 */}
<View style={styles.line} />
</View>
)
}
}
/**
* 单个tab
*/
class Tab extends Component {
constructor(props) {
super(props)
this.state = {
selected: 0 //选中的下标
}
}
render() {
return (
<TouchableHighlight
underlayColor='white'
onPress={() => {
this.props.onTabPress && this.props.onTabPress(this.props.index)
}}
style={[{ height: this.props.tabHeight, width: this.props.tabWidth }, styles.tabStyle]}>
<Text style={{
textAlign: 'center',
color: this.state.selected === this.props.index ? this.props.selectedTextColor : this.props.unselectedTextColor,
...this.props.style,
}}>
{this.props.title}
</Text>
</TouchableHighlight>
)
}
//修改选中的index
changeState = (index) => {
this.setState({
selected: index
})
}
}
const styles = StyleSheet.create({
line: {
backgroundColor: '#cccccc',
width: screenWidth,
height: 1
},
tabLine: {
position: 'absolute',
bottom: 0,
height: 2,
backgroundColor: '#FFE97A',
},
tabStyle: {
justifyContent: 'center',
alignContent: 'center'
}
})
接着是继续封装集成了顶部tab与底部ScrollView的组件,我命名为ScrollTabView
import React, { Component } from 'react';
import {
View,
StyleSheet,
ScrollView,
Dimensions
} from 'react-native';
import TabIndicator from './TabIndicator'
const screenWidth = Dimensions.get('window').width;
const screenHeight = Dimensions.get('window').height;
//集成TabIndicator和需要滚动的子视图
export default class ScrollTabView extends Component {
//这里的属性基本就是TabIndicator的除了titles,这个是所有tab的名称,当然最后也是传给TabIndicator的
static propTypes = {
tabBarActiveTextColor: React.PropTypes.string,
tabBarInactiveTextColor: React.PropTypes.string,
titles: React.PropTypes.array,
tabTextStyle: React.PropTypes.any,
tabBarUnderlineStyle: React.PropTypes.any,
tabHeight: React.PropTypes.number,
};
static defaultProps = {
tabBarActiveTextColor: '#686868',
tabBarInactiveTextColor: '#cccccc',
titles: [],
tabHeight: 50,
};
render() {
return (
<View style={styles.container}>
<TabIndicator
ref={(r) => this.tab = r}
tabNames={this.props.titles}
tabBarUnderlineStyle={this.props.tabBarUnderlineStyle}
tabHeight={this.props.tabHeight}
tabTextStyle={this.props.tabTextStyle}
tabBarActiveTextColor={this.props.tabBarActiveTextColor}
tabBarInactiveTextColor={this.props.tabBarInactiveTextColor}
onTabPress={(index) => {
this.sv.scrollTo({ x: index * screenWidth, y: 0, animated: true })
}}
/>
<ScrollView
ref={(r) => this.sv = r}
pagingEnabled
horizontal
onScroll={(e) => {
this.tab.swipeToTrans(e.nativeEvent.contentOffset.x / screenWidth)
}}
>
{/* 要展示的滚动视图 */}
{this.props.children}
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
width: screenWidth,
height: screenHeight,
}
})
最后就是测试代码了
//tab名称
const titles = ['科技', '热点', '饮食', '交通', '城市', '热点', '军事', '人文']
//tab对应的view
const views = []
componentDidMount() {
views.push(<View style={{ width: screenWidth, backgroundColor: '#dfdfdf' }} />)
views.push(<View style={{ width: screenWidth, backgroundColor: '#green' }} />)
views.push(<View style={{ width: screenWidth, backgroundColor: '#yellow' }} />)
views.push(<View style={{ width: screenWidth, backgroundColor: '#blue' }} />)
views.push(<View style={{ width: screenWidth, backgroundColor: '#pink' }} />)
views.push(<View style={{ width: screenWidth, backgroundColor: '#red' }} />)
views.push(<View style={{ width: screenWidth, backgroundColor: '#gray' }} />)
views.push(<View style={{ width: screenWidth, backgroundColor: '#purple' }} />)
}
render() {
return (
<ScrollableTabView
tabHeight={40}
tabBarActiveTextColor='#686868'
tabBarInactiveTextColor='#cccccc'
titles={titles}
>
{/* 展示页面 */}
{views}
</ScrollableTabView>
)
}