您的位置:首页 > Web前端 > React

React Native带你实现scrollable-tab-view(五)

2017-09-07 22:22 615 查看
上一节React Native带你实现scrollable-tab-view(三)中我们最后实现了我们scrollable-tab-view的效果为:



比如有很多页面,我们的tabview需要跟随scrollview的滑动而滑动:



我们现在用的还是默认的tabview,比如我们多加一些页面,



这样肯定不行,我们需要的是上面的那种效果,所以每个view的flex=1这种模式肯定不适合了,我们需要用一个scrollview去包涵tab,使它也能滑动,所以也算是scrollable-tab-view的一个难点了,好啦~ 我们来一步一步实现一下scrollable-tab-view中的ScrollableTabBar.js。

我们先创建一个view叫ScrollableTabBar.js:

/**
* @author YASIN
* @version [React-Native Pactera V01, 2017/9/7]
* @date 17/2/23
* @description ScrollableTabBar
*/
import React, {
Component
} from 'react';
import {
View,
Text,
StyleSheet,
} from 'react-native';
export default class ScrollableTabBar extends Component {
// 构造
constructor(props) {
super(props);
// 初始状态
this.state = {};
}

render() {
return (
<View>
<Text>ScrollableTabBar</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {}
});


然后替换掉index文件中的DefaultTabBar组件:

/**
* 渲染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 (
<ScrollableTabBar
{...tabParams}
style={[{width: this.state.containerWidth}]}
onTabClick={(page)=>this.goToPage(page)}
/>
);
}




然后我们开始实现ScrollableTabBar组件:

我们用一个scrollview去包裹tab,然后其它的东西直接copy一份DefaultTabBar的内容:

/**
* @author YASIN
* @version [React-Native Pactera V01, 2017/9/7]
* @date 17/2/23
* @description ScrollableTabBar
*/
import React, {
Component
} from 'react';
import {
View,
Text,
StyleSheet,
Dimensions,
TouchableOpacity,
ScrollView,
Animated,
} from 'react-native';
const screenW = Dimensions.get('window').width;
const screenH = Dimensions.get('window').height;
export default class ScrollableTabBar extends Component {
// 构造
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: 50,
position: 'absolute',
bottom: 0,
left,
};
return (
<View style={[styles.container, this.props.style]}>
<ScrollView
automaticallyAdjustContentInsets={false}
ref={(scrollView) => {
this._scrollView = scrollView;
}}
horizontal={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
directionalLockEnabled={true}
bounces={false}
scrollsToTop={false}
>
<View
style={styles.tabContainer}
>
{tabs.map((tab, index)=> {
return this._renderTab(tab, index, index === this.props.activeTab);
})}
<Animated.View
style={[styles.tabLineStyle, tabStyle]}
/>
</View>
</ScrollView>
</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,
height: 50,
},
tabContainer: {
flexDirection: 'row',
alignItems: 'center',
},
tabLineStyle: {
height: 1,
backgroundColor: 'navy',
},
tabStyle: {
height: 49,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 20,
},
});


然后我们运行代码:



可以看到,有点那种感觉了,但是我们目前看到的东西需要解决的就是:

1、底部那个线条长度计算(目前是定死了一个值)

render() {
....
let tabStyle = {
width: 50,
position: 'absolute',
bottom: 0,
left,
};
...


我们线条的长度应该等于当前tab的宽度。

2、我们页面滑动的时候,线条指向有问题。

好了~ 我们一个一个的解决,我们把下标线view的left跟宽度用两个单独的动画控制,然后监听内容scrollview的scrollValue动画滑动改变控制left和控制width的动画值:

export default class ScrollableTabBar extends Component {
// 构造
constructor(props) {
super(props);
// 初始状态
this.state = {
_leftTabUnderline: new Animated.Value(0),
_widthTabUnderline: new Animated.Value(0),
};
}

render() {
let {containerWidth, tabs, scrollValue}=this.props;
//给传过来的动画一个插值器
let tabStyle = {
width: this.state._widthTabUnderline,
position: 'absolute',
bottom: 0,
left: this.state._leftTabUnderline,
};
return (
<View style={[styles.container, this.props.style]}>
<ScrollView
....
>
<View
style={styles.tabContainer}
>
...
<Animated.View
style={[styles.tabLineStyle, tabStyle]}
/>
</View>
</ScrollView>
</View>
);
}


然后跟前一节我们实现的DefaultTabBar.js一样,监听scrollValue动画:

componentDidMount() {
this.props.scrollValue.addListener(this._updateView);
}


/**
* 根据scrollview的滑动改变底部线条的宽度跟left
* @param value
* @private
*/
_updateView = ({value = 0}) => {
//因为value 的值是[0-1-2-3-4]变换
const position = Math.floor(value);
//取小数部分
const offset = value % 1;
const tabCount = this.props.tabs.length;
const lastTabPosition = tabCount - 1;
//如果没有tab||(有bounce效果)直接return
if (tabCount === 0 || value < 0 || value > lastTabPosition) {
return;
}
console.log('position==>' + position + ' offset==>' + offset);
}


我们运行然后从第一页滑动到第二页:



可以看到,我们position是从(0-1)而offset为0–>1的一个小数,所以我们:

1、底部线条view的宽度应该为(第position页tab宽度)*(1-offset)+(第positon+1页tab的宽度)*offset。

2、底部线条view的left应该为(第position页tab的x轴)+(第position+1页tab的宽度)*offset。

所以我们接下来要做的就是通过o nLayout方法计算出每个tab的x轴跟宽度然后保存起来。

我们在渲染tabbar的时候提供view o nLayout方法去测量view:

/**
* 渲染tab
* @param name 名字
* @param page 下标
* @param isTabActive 是否是选中的tab
* @private
*/
_renderTab(name, page, isTabActive) {
....
let self = this;
return (
<TouchableOpacity
.....
onLayout={(event)=>this._onMeasureTab(page, event)}
>
<Text style={[tabTextStyle]}>{name}</Text>
</TouchableOpacity>
);
}


每次测量完毕后去更新tab底部线view的宽度:

/**
* 测量tabview
* @param page 页面下标
* @param event 事件
* @private
*/
_onMeasureTab(page, event) {
let {nativeEvent:{layout:{x, width}}}=event;
this._tabsMeasurements[page] = {left: x, right: width + x, width: width};
this._updateView({value: this.props.scrollValue._value})
}


按照我们前面所说的“1、底部线条view的宽度应该为(第position页tab宽度)*(1-offset)+(第positon+1页tab的宽度)*offset。”所以我们必须要拿到tab的测量值,并且有下一个tab,或者特殊情况就是到最后一个tab了,最后才去做改变width的操作。

/**
* 根据scrollview的滑动改变底部线条的宽度跟left
* @param value
* @private
*/
_updateView = ({value = 0}) => {
//因为value 的值是[0-1-2-3-4]变换
const position = Math.floor(value);
//取小数部分
const offset = value % 1;
const tabCount = this.props.tabs.length;
const lastTabPosition = tabCount - 1;
//如果没有tab||(有bounce效果)直接return
if (tabCount === 0 || value < 0 || value > lastTabPosition) {
return;
}
if (this._necessarilyMeasurementsCompleted(position, position === tabCount - 1)) {
this._updateTabLine(position, offset);
}
}


/**
* 判断是否需要跟新的条件是否初始化
* @param position
* @param isLast 是否是最后一个
* @private
*/
_necessarilyMeasurementsCompleted(position, isLast) {
return (
this._tabsMeasurements[position] &&
(isLast || this._tabsMeasurements[position + 1])
);
}


然后套去我们前面说的公式:

(第position页tab宽度)*(1-offset)+(第positon+1页tab的宽度)*offset

但是有一种特殊情况,就是position+1不存在,所以我们要做好判断:

/**
* 更新底部线view的left跟width
* @param position
* @param offset
* @private
*/
_updateTabLine(position, offset) {
//当前tab的测量值
const currMeasure = this._tabsMeasurements[position];
//position+1的tab的测量值
const nextMeasure = this._tabsMeasurements[position + 1];
let width = currMeasure.width * (1 - offset);
if (nextMeasure) {
width += nextMeasure.width * offset;
}
this.state._widthTabUnderline.setValue(width);
}


然后我们运行代码:



可以看到,我们滑动的时候tab底部线条的宽度再改变,宽度=我们的tab的宽度,好啦!! 我们接着只要使线条view的left跟着scrollview滑动而改变到对应的tab位置就可以了。

我们继续套入公式:

(第position页tab的x轴)+(第position+1页tab的宽度)*offset

/**
* 更新底部线view的left跟width
* @param position
* @param offset
* @private
*/
_updateTabLine(position, offset) {
//当前tab的测量值
const currMeasure = this._tabsMeasurements[position];
//position+1的tab的测量值
const nextMeasure = this._tabsMeasurements[position + 1];
let width = currMeasure.width * (1 - offset);
let left= currMeasure.left;
if (nextMeasure) {
width += nextMeasure.width * offset;
left+=nextMeasure.width*offset;
}
this.state._leftTabUnderline.setValue(left);
this.state._widthTabUnderline.setValue(width);
}


然后运行代码:



好啦! 可以看到,我们底部线条view可以跟随了,到这我们还差最后一步,那就是当我们点击tab的时候如果后面的tab被遮住了就得向后移动。

有点晚了,该洗洗睡啦~~

欢迎入群,欢迎交流,大牛勿喷,下一节见!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: