React-Native ScrollView自定义横向滑动进度条
React-Native ScrollView自定义横向滑动进度条
概要
本篇文章概述了通过React-Native实现一个允许自定义横向滑动进度条的ScrollView组件。
需求
开发一个首页摆放菜单入口的ScrollView可滑动组件(类似某淘首页上的菜单效果),允许自定义横向滑动进度条,且内部渲染的菜单内容支持自定义展示的行数和列数,在内容超出屏幕后,渲染顺序为纵向由上至下依次排列。
自定义滑动进度条
确定参数
首先,让我们确定一下自定义滑动进度条需要哪些参数来支持:
- 初始位置时,确定显示进度的条的宽度(barWidth)
- 滑动进度,以此来确定上面这个条的位置现在应该到哪里了(marLeftAnimated)
计算参数
1.想要确定显示进度的条的宽度(barWidth),那么必须先知道三个值:
- ScrollView总宽度(containerStyle传入)
- 进度条背景的宽度(indicatorBgStyle传入)
- ScrollView内部内容总宽度(childWidth,通过onContentSizeChange方法测量)
然后我们就可以进行如下计算,这样得到的_barWidth就是显示进度的条的宽度(barWidth):
let _barWidth = (this.props.indicatorBgStyle.width * this.props.containerStyle.width) / this.state.childWidth;
2.想要确定显示进度的条的位置(marLeftAnimated),那么必须先知道两个值:
- ScrollView可滑动距离(scrollDistance)
- 进度部分可滑动距离(leftDistance)
然后我们就可以进行如下定义,这样得到的marLeftAnimated,输出值即为进度条的距左距离:
let scrollDistance = this.state.childWidth - this.props.containerStyle.width
...
//显示滑动进度部分的距左距离
let leftDistance = this.props.indicatorBgStyle.width - _barWidth;
const scrollOffset = this.state.scrollOffset
this.marLeftAnimated = scrollOffset.interpolate({
inputRange: [0, scrollDistance], //输入值区间为内容可滑动距离
outputRange: [0, leftDistance], //映射输出区间为进度部分可改变距离
extrapolate: 'clamp', //钳制输出值
useNativeDriver: true,
})
滑动进度条的实现
通过Animated.View,定义绝对位置,将两个条在Z轴上下重叠一起。
<View style={[{alignSelf:'center'},this.props.indicatorBgStyle]}>
<Animated.View
style={[this.props.indicatorStyle,{
position: 'absolute',
width: this.state.barWidth,
top: 0,
left: this.marLeftAnimated,
}]}
/>
</View>
之后就通过onSroll事件获取滑动偏移量,然后通过偏移量改变动画的值,这里我就不多说了,不明白的可以看我上一篇文章。
首页定制菜单
确定参数
首先,让我们确定一下实现首页定制菜单需要哪些参数来支持:
- 列数量(columnLimit)
- 行数量(rowLimit)
渲染方式
根据行列数量,决定每屏的菜单总数。根据行数量,决定渲染结果数组里有几组,一行就是一组。
let optionTotalArr = []; //存放所有option样式的数组
//根据行数,声明用于存放每一行渲染内容的数组
for( let i = 0; i < rowLimit; i++ ) optionTotalArr.push([])
1.没超出屏幕时,确定渲染行的方式如下:
if(index < columnLimit * rowLimit){
//没超出一屏数量时,根据列数更新行标识
rowIndex = parseInt(index / columnLimit)
}
2.超出屏幕时,确定渲染行的方式如下:
//当超出一屏数量时,根据行数更新行标识
rowIndex = index % rowLimit;
遍历输出
根据行数,遍历存放计算后的行内容数组。
optionTotalArr[rowIndex].push(
<TouchableOpacity
key={index}
activeOpacity={0.7}
style={[styles.list_item,{width:size}]}
onPress={()=>alert(item.name)}
>
<View style={{width:size-20,backgroundColor:'#FFCC00',alignItems:'center',justifyContent:'center'}}>
<Text style={{ fontSize:18, color:'#333',marginVertical:20}}>{item.name}</Text>
</View>
</TouchableOpacity>
)
效果图
源码
IndicatorScrollView.js
import React, { PureComponent } from 'react';
import {
StyleSheet,
View,
ScrollView,
Animated,
Dimensions,
} from 'react-native';
import PropTypes from 'prop-types';
const { width, height } = Dimensions.get('window');
export default class IndicatorScrollView extends PureComponent {
static propTypes = {
//最外层样式(包含ScrollView及滑动进度条的全部区域
containerStyle: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
]),
//ScrollView的样式
style: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
]),
//滑动进度条底部样式
indicatorBgStyle: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
]),
//滑动进度条样式
indicatorStyle: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
]),
}
static defaultProps = {
containerStyle: { width: width },
style: {},
indicatorBgStyle:{
width: 200,
height: 20,
backgroundColor: '#ddd'
},
indicatorStyle:{
height:20,
backgroundColor:'#000'
},
}
constructor(props) {
super(props);
this.state = {
//滑动偏移量
scrollOffset: new Animated.Value(0),
//ScrollView子布局宽度
childWidth: this.props.containerStyle.width,
//显示滑动进度部分条的长度
barWidth: props.indicatorBgStyle.width / 2,
};
}
UNSAFE_componentWillMount() {
this.animatedEvent = Animated.event(
[{
nativeEvent: {
contentOffset: { x: this.state.scrollOffset }
}
}]
)
}
componentDidUpdate(prevProps, prevState) {
//内容可滑动距离
let scrollDistance = this.state.childWidth - this.props.containerStyle.width
if( scrollDistance > 0 && prevState.childWidth != this.state.childWidth){
let _barWidth = (this.props.indicatorBgStyle.width * this.props.containerStyle.width) / this.state.childWidth;
this.setState({
barWidth: _barWidth,
})
//显示滑动进度部分的距左距离
let leftDistance = this.props.indicatorBgStyle.width - _barWidth;
const scrollOffset = this.state.scrollOffset
this.marLeftAnimated = scrollOffset.interpolate({
inputRange: [0, scrollDistance], //输入值区间为内容可滑动距离
outputRange: [0, leftDistance], //映射输出区间为进度部分可改变距离
extrapolate: 'clamp', //钳制输出值
useNativeDriver: true,
})
}
}
render() {
return (
<View style={[styles.container,this.props.containerStyle]}>
<ScrollView
style={this.props.style}
horizontal={true} //横向
alwaysBounceVertical={false}
alwaysBounceHorizontal={false}
showsHorizontalScrollIndicator={false} //自定义滑动进度条,所以这里设置不显示
scrollEventThrottle={0.1} //滑动监听调用频率
onScroll={this.animatedEvent} //滑动监听事件,用来映射动画值
scrollEnabled={ this.state.childWidth - this.props.containerStyle.width>0 ? true : false }
onContentSizeChange={(width,height)=>{
if(this.state.childWidth != width){
this.setState({ childWidth: width })
}
}}
>
{this.props.children??
<View
style={{ flexDirection: 'row', height: 200 }}
>
<View style={{ width: 300, backgroundColor: 'red' }} />
<View style={{ width: 300, backgroundColor: 'yellow' }} />
<View style={{ width: 300, backgroundColor: 'blue' }} />
</View>
}
</ScrollView>
{this.state.childWidth - this.props.containerStyle.width>0?
<View style={[{alignSelf:'center'},this.props.indicatorBgStyle]}>
<Animated.View
style={[this.props.indicatorStyle,{
position: 'absolute',
width: this.state.barWidth,
top: 0,
left: this.marLeftAnimated,
}]}
/>
</View>:null
}
</View>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
Scroll.js
import React, { Component } from 'react';
import {
StyleSheet,
View,
Dimensions,
TouchableOpacity,
Text,
} from 'react-native';
import IndicatorScrollView from '../../component/scroll/IndicatorScrollView';
const { width, height } = Dimensions.get('window');
const columnLimit = 4; //option列数量
const rowLimit = 2; //option行数量
// 编写UI组件
export default class Scroll extends Component {
constructor(props) {
super(props);
this.state = {
};
this.itemArr = [
{
name: '1'
},
{
name: '2'
},
{
name: '3'
},
{
name: '4'
},
{
name: '5'
},
{
name: '6'
},
{
name: '7'
},
{
name: '8'
},
{
name: '9'
},
{
name: '10'
},
{
name: '11'
},
{
name: '12'
}
]
}
renderOption(){
let size = (width-20)/columnLimit; //每个option的宽度
let optionTotalArr = []; //存放所有option样式的数组
//根据行数,声明用于存放每一行渲染内容的数组
for( let i = 0; i < rowLimit; i++ ) optionTotalArr.push([])
this.itemArr.map((item,index) => {
let rowIndex = 0; //行标识
if(index < columnLimit * rowLimit){
//没超出一屏数量时,根据列数更新行标识
rowIndex = parseInt(index / columnLimit)
}else{
//当超出一屏数量时,根据行数更新行标识
rowIndex = index % rowLimit;
}
optionTotalArr[rowIndex].push(
<TouchableOpacity
key={index}
activeOpacity={0.7}
style={[styles.list_item,{width:size}]}
onPress={()=>alert(item.name)}
>
<View style={{width:size-20,backgroundColor:'#FFCC00',alignItems:'center',justifyContent:'center'}}>
<Text style={{ fontSize:18, color:'#333',marginVertical:20}}>{item.name}</Text>
</View>
</TouchableOpacity>
)
})
return(
<View
style={{flex:1,justifyContent:'center',paddingHorizontal:10}}
>
{
optionTotalArr.map((item,index)=>{
return <View key={index} style={{flexDirection:'row'}}>{item}</View>
})
}
</View>
)
}
render() {
return (
<View style={styles.container}>
<View style={{flex:1}}/>
<IndicatorScrollView
containerStyle={styles.list_style}
indicatorBgStyle={{marginBottom:10,borderRadius:2,width:40,height:4,backgroundColor:'#BFBFBF'}}
indicatorStyle={{borderRadius:2,height:4,backgroundColor:'#CC0000'}}
>
{this.renderOption()}
</IndicatorScrollView>
<View style={{flex:1}}/>
</View >
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
backgroundColor: '#fff',
},
list_style:{
flex: 1,
width: width,
backgroundColor:'#6699FF'
},
list_item:{
marginVertical:20,
justifyContent:'center',
alignItems:'center',
},
});
注:本文为作者原创,转载请注明作者及出处。
本文地址:https://blog.csdn.net/JJochen/article/details/112544264
上一篇: 支付宝生活缴费账单怎么删除 支付宝生活缴费账单删除教程
下一篇: 下回看好了号码再打