起因
最近做项目时,碰到了一个需求:客户需要修改列表滑动条的颜色。既然存在滑动条,肯定就是ScrollView、FlatList这种长列表。于是去官网(需科学上网)找了下相应的API。发现并没有比较友好的API支持滑动条的颜色修改。唯一支持的indicatorStyle属性,也仅实用IOS,且也无法实用自定义的颜色~~没办法,就只能自己动手做了。
分析
既然要自己动手重新做一个滑动条,那么我们可以先来参考下原有的滑动条效果。
这里我们先模拟200条数据。放到FlatList中。
componentDidMount() {
const { data } = this.state;
for (let i = 0; i < 200; i += 1) {
data.push(`Data ${i}`);
}
...
}
render(){
...
<FlatList
data={data}
keyExtractor={item => (item)}
initialNumToRender={9}
renderItem={({ item }) => (
<Text style={styles.item} >
{item}
</Text>)}
/>
}

从滑动效果图中可以看出:
- 列表静止状态时,右边的滑动条是隐藏的。
- 右边滑动条随着列表进行移动。
- 从移动状态变为静止状态时,滑动条有一个过渡消失动画。
第1点直接将滑动条默认设置为隐藏就好,第3点添加一个基本的透明动画即可。
这里需要重点考虑第2点。
先定义一下基本观念:组件高度、内容真实高度、滑动条高度、滑动条可滑动的高度。如图:

从正常交互而言,右边滑动条滑动到一半时,左边组件也会展示整体内容的一半。右边滑动条滑动到底部时,左边组件也会展示完全部整体内容。所以:contain Height/content Height=indicetor Height/slider Height。sliderHeight的高度就是组件的高度。于是我们可推出indicatorHeight:indicatorHeight=containHeight^2 / sliderHeight。滑动条的滑动距离与上述类似:列表滑动距离/contentHeight=滑动条滑动距离/sliderHeight。而我们列表滑动距离是已知的。所以滑动条的滑动距离就应该是:滑动条滑动距离=列表滑动距离*containHeight/contentHeight。
render() {
const { data, extraData, keyExtractor, renderItem, style, indicatorStyle } = this.props;
const { indicator, contentHeight, containHeight } = this.state;
const indicatorHeight = contentHeight > containHeight ? containHeight * containHeight / contentHeight : 0;
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<FlatList
onContentSizeChange={(width, height) => {
// 获取内容真实高度
this.setState({ contentHeight: height });
}}
onLayout={({ nativeEvent: { layout: { height } } }) => {
// 获取当前组件高度
this.setState({ containHeight: height });
}}
// 通过列表的滑动事件,得到具体的滑动值
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: indicator } } }])}
...
/>
<Animated.View style={[
styles.indicator, {
...,
height: indicatorHeight,
transform: [{ translateY: Animated.multiply(indicator, containHeight / contentHeight ) }]
}]}
/>
</View>
);

滑动条是可进行移动了,但是还默认隐藏,以及停止滑动的消失过渡动画。我们将其补全加上去。最终代码如下:
constructor(props) {
super(props);
this.state = {
indicator: new Animated.Value(0), // 滑动移动的距离
opacity: new Animated.Value(0), // 默认隐藏
contentHeight: 1, // 内容真实高度
containHeight: 0// 组件可见高度
};
}
startAnimation=toValue => {
Animated.timing(
this.state.opacity,
{
toValue,
duration: 1000,
}
).start();
}
render() {
const { data, extraData, keyExtractor, renderItem, style, indicatorStyle } = this.props;
const { indicator, contentHeight, containHeight, opacity } = this.state;
const indicatorHeight = contentHeight > containHeight ? containHeight * containHeight / contentHeight : 0;
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<FlatList
onContentSizeChange={(width, height) => {
// 获取内容真实高度
this.setState({ contentHeight: height });
}}
onLayout={({ nativeEvent: { layout: { height } } }) => {
// 获取当前组件高度
this.setState({ containHeight: height });
}}
// 通过列表的滑动事件,将滑动偏移量进行一个映射
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: indicator } } }])}
// 滑动触摸结束时隐藏
onMomentumScrollEnd={() => this.startAnimation(0, 1000)}
// 滑动触摸开始时展示
onScrollBeginDrag={() => this.startAnimation(1)}
...
/>
<Animated.View style={[
styles.indicator, {
...
opacity,
height: indicatorHeight,
transform: [{
translateY: Animated.multiply(indicator, containHeight / contentHeight)
}]
}]}
/>
</View>
);
}

至此,效果已经全部ok~~~