React-Native ScrollView自定义横向滑动进度条
概要需求自定义滑动进度条确定参数计算参数滑动进度条的实现 首页定制菜单确定参数渲染方式遍历输出 效果图源码IndicatorScrollView.jsScroll.js概要
本篇文章概述了通过React-Native实现一个允许自定义横向滑动进度条的ScrollView组件。
需求
开发一个首页摆放菜单入口的ScrollView可滑动组件(类似某淘首页上的菜单效果),允许自定义横向滑动进度条,且内部渲染的菜单内容支持自定义展示的行数和列数,在内容超出屏幕后,渲染顺序为纵向由上至下依次排列。
Animated 动画 点此进入学习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.scrollOffsetthis.marLeftAnimated = scrollOffset.interpolate({inputRange: [0, scrollDistance], //输入值区间为内容可滑动距离outputRange: [0, leftDistance], //映射输出区间为进度部分可改变距离extrapolate: 'clamp', //钳制输出值useNativeDriver: true,})
滑动进度条的实现
通过Animated.View,定义绝对位置,将两个条在Z轴上下重叠一起。
<View style={[{alignSelf:'center'},this.props.indicatorBgStyle]}><Animated.Viewstyle={[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.widthif( 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.scrollOffsetthis.marLeftAnimated = scrollOffset.interpolate({inputRange: [0, scrollDistance], //输入值区间为内容可滑动距离outputRange: [0, leftDistance], //映射输出区间为进度部分可改变距离extrapolate: 'clamp', //钳制输出值useNativeDriver: true,})}}render() {return (<View style={[styles.container,this.props.containerStyle]}><ScrollViewstyle={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.Viewstyle={[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(<Viewstyle={{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',},});
注:本文为作者原创,转载请注明作者及出处。