优秀的编程知识分享平台

网站首页 > 技术文章 正文

封装Picker 组件(封装列表)

nanyue 2024-07-23 13:40:12 技术文章 10 ℃
import {
  View,
  Text,
  TouchableWithoutFeedback,
  TouchableOpacity,
  FlatList,
} from 'react-native'
import React,{
  PureComponent,
  Component,
} from 'react'
import _ from 'lodash'
import styleSheet, {getThemeColor} from '../utils/styleSheet'
import {Callback} from "modern/types/lang";
import Modal from './Modal'
import {getScreenHeight, getScreenWidth} from "modern/consts/ui-common";

const styles = styleSheet.create({
  container:{
    flex:1,
    backgroundColor: 0x00000074,
    alignItems: 'center',
    justifyContent: 'flex-end',
  },
  contentView:{
    flexDirection:'row',
    justifyContent: 'space-around',
    backgroundColor: 'white'
  },
  stage:{
    top:0,
    bottom:0,
    left:0,
    right:0,
    position:'absolute',
    zIndex:10,
    flexDirection:'row',
    justifyContent: 'space-around',
  },
  maskView:{
    left:0,
    right:0,
    position:'absolute',
    top:0,
    bottom:0,
    zIndex:0,
    justifyContent:'space-between'
  },
  emptyView:{
    height:50,
    flexDirection:'row',
    justifyContent:'space-between',
    paddingHorizontal:20,
    backgroundColor: 'white',
  },
  maskStyle:{
    width:'100%',
    // backgroundColor:'rgba(232,232,232,0.9)',
    borderColor:"rgba(232,232,232,0.9)",
    backgroundColor:'white'
  },
  midMaskStyle:{
    backgroundColor:'#f9f9f9'
  },
  headerBtnView:{
    height:50,justifyContent: 'center'
  },
  cancelText:{
    fontSize:16,color:'gray'
  }
})

function DefaultView (){
  return (
    <View style={{flex:1,justifyContent:'center',alignItems: 'center'}}>
        <Text>暂无数据</Text>
    </View>
  )
}
interface ISHeadProps {
  customHead:any,
  confirm:Callback,
  hide:Callback,
  headOptions:any
}
const Head:React.FC<ISHeadProps> = React.memo(({customHead,confirm,hide,headOptions})=>{
    if(customHead && _.isFunction(customHead)){
      return customHead(confirm,hide)
    }
    const {
      cancelTextStyle={},
      cancelBtnView={},
      confirmBtnView={},
      confirmTextStyle={},
      leftText='取消',
      rightText='确定',
      headerContainView={},
    } = headOptions
    return <View style={[styles.emptyView,headerContainView]}>
      <TouchableOpacity style={[styles.headerBtnView,cancelBtnView]} onPress={hide}>
        <Text style={[styles.cancelText,cancelTextStyle]}>{leftText}</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={confirm} style={[styles.headerBtnView,confirmBtnView]}>
        <Text style={[{color:getThemeColor(),fontSize:16},confirmTextStyle]}>{rightText}</Text>
      </TouchableOpacity>
    </View>
})
interface ISEmpty {
  hide:Callback
}
const Empty:React.FC<ISEmpty> = React.memo(({hide})=>{
  return <TouchableWithoutFeedback  onPress={hide}>
    <View style={{flex:1,width:getScreenWidth()}} />
  </TouchableWithoutFeedback>
})
interface ISMaskView {
  maxLength:number,
  itemHeihght:number,
  maskOptions:any,
}
const MaskView:React.FC<ISMaskView> = React.memo(({maxLength,itemHeihght,maskOptions={}})=>{
  const {
    upMaskView,
    bottomMaskView,
    midMaskView
  } = maskOptions
  const mid = Math.floor(maxLength/2)
  return  <View style={[styles.maskView]}>
    <View style={[styles.maskStyle,{height:itemHeihght*mid,borderBottomWidth:0.5,},upMaskView]}/>
    <View style={[styles.midMaskStyle,{height:itemHeihght},midMaskView]} />
    <View style={[styles.maskStyle,{height:itemHeihght*mid,borderTopWidth:0.5,},bottomMaskView]}/>
  </View>
})
interface ISourceType {
  id:string,
  name:string,
  sub?:ISourceType[]
  [propname:string]:any,
}
interface ISitemOptons  {
  itemHeihght?:number,
  maxLength?:5 | 7 | 9,
  ActiveTextStyle?:any,
  normalTextStyle?:any,
}
interface ISProps {
  sourceData:ISourceType[],
  cancel?:Callback,
  confirm:Callback,
  selectData:ISourceType[],
  pickerType:'scroll' | 'click' | 'all',
  customHead:any,
  headOptions:any,
  maskOptions:any,
  itemOptons:ISitemOptons,
  isLinkage:boolean,
  renderListEmptyComponent:Callback,
  emptyOptions:any,
  wrapOptions:any,
  EmptyView:any
}
interface ISState {
  visible:boolean,
  floor:number,
}
function WithHeadAndMethod(WrapComponent:any){
    return class PickerAlertView extends PureComponent<ISProps,ISState>{
      public selectedData:ISourceType[] =[]
      public static defaultProps = {
        itemOptons:{},
        selectData:[],
        headOptions:{},
        pickerType:'all',
        maskOptions:{},
        isLinkage:false,
        emptyOptions:{},
        wrapOptions:{
          stageView:{}
        }
      }
      constructor(props:ISProps) {
        super(props);
        this.state = {
          visible:false,
          floor:this.getMaxFloor(),
        }
      }
      public getMaxFloor = ()=>{
        const { sourceData,isLinkage } = this.props
        if(_.isEmpty(sourceData)) return 0
        if(isLinkage) return sourceData.length
        let floor:number = 1
        function treeData(arr:any){
          const sub:any = arr.find((item:any)=>item?.sub?.length > 0)
          if(sub){
            floor = floor + 1
            let arr2:any = []
            arr.forEach((item:any)=>{
              if(item?.sub?.length>0){
                arr2 = arr2.concat(item.sub)
              }
            })
            treeData(arr2)
          }
        }
        treeData(sourceData)
        return floor
      }
      public show = ()=>{
        this.setState({visible:true})
      }
      public confirm = ()=>{
        const { confirm=_.noop } = this.props
        this.hide()
        confirm(this.selectedData)
      }
      public hide = ()=>{
        const { cancel=_.noop } = this.props
        cancel()
        this.setState({visible:false})
      }
      public getSelected = ()=>this.selectedData
      private setSelected = (item:ISourceType,floor:number)=>{
        this.selectedData[floor] = item
      }
      public componentDidUpdate(preProps,prevState,snapshot){
        if(!_.isEqual(this.props.sourceData,preProps.sourceData)){
          this.setState({
            floor:this.getMaxFloor()
          })
        }
      }
      public render(){
        const {
          visible,
          floor,
        } = this.state
        const {
          selectData,
          sourceData,
          pickerType,
          customHead,
          headOptions,
          maskOptions,
          itemOptons,
          isLinkage,
          renderListEmptyComponent,
          emptyOptions,
          wrapOptions,
          EmptyView = DefaultView
        } = this.props
        const { stageView } = wrapOptions
        if(!visible) return null
        const {
          maxLength=9,
          itemHeihght=50,
          ActiveTextStyle={},
          normalTextStyle={}
        }  = itemOptons
        return (
          <Modal
            visible={visible}
            animationType="slide"
            transparent={true}
          >
            <View
              style={styles.container}
            >
              <Empty hide={this.hide}/>
              <View style={{ width:getScreenWidth()}}>
                <Head
                  headOptions={headOptions}
                  hide={this.hide}
                  confirm={this.confirm}
                  customHead={customHead}
                />
                <View  style={[{height:itemHeihght*maxLength},styles.contentView]}>
                  <View style={[styles.stage,stageView]}>
                    {
                      _.isEmpty(sourceData) ?<EmptyView/>:
                        <WrapComponent
                          sourceList={sourceData}
                          setSelect={this.setSelected}
                          floor={floor}
                          currentFloor={0}
                          maxLength={maxLength}
                          defaultSelected = {selectData}
                          itemHeihght={itemHeihght}
                          ActiveTextStyle={ActiveTextStyle}
                          normalTextStyle={normalTextStyle}
                          pickerType={pickerType}
                          originData={sourceData}
                          isLinkage={isLinkage}
                          renderListEmptyComponent={renderListEmptyComponent}
                          emptyOptions={emptyOptions}
                        />
                    }

                  </View>
                  <MaskView
                    maxLength={maxLength}
                    itemHeihght={itemHeihght}
                    maskOptions={maskOptions}
                  />
                </View>
              </View>
            </View>
          </Modal>
        )
      }

    }
}

interface ISItemProps {
  sourceList:ISourceType[],
  setSelect:Callback,
  floor:number,
  currentFloor:number,
  maxLength: 5 | 7 | 9,
  defaultSelected:ISourceType[],
  itemHeihght:number,
  ActiveTextStyle:any,
  normalTextStyle:any,
  pickerType:string,
  isLinkage:boolean,
  originData:any,
  renderListEmptyComponent:Callback,
  emptyOptions:any
}
interface ISItemState {
  nextData:ISourceType[],
  currentChoose:ISourceType,
  initialScrollIndex:number,
  renderList:any[],
  surerefresh:any
}
class PickerAlertViewItem extends PureComponent<ISItemProps,ISItemState> {
  public scrollView:any
  public flatEl:any
  public currentItem:any
  public itemEL:any
  public config:any = {
    waitForInteraction: false,
    itemVisiblePercentThreshold:100,
  }
  public  constructor(props:ISItemProps) {
    super(props);
    this.state = {
      nextData:this.defaultRenderList(),
      currentChoose:this.getDefaultChoose(),
      initialScrollIndex:this.getInitialScrollIndex(),
      renderList:this.flatListData(),
      surerefresh:null,
    }
  }
  public getInitialScrollIndex = ()=>{
    const {defaultSelected,currentFloor,sourceList } = this.props
    if(_.isEmpty(defaultSelected)) return 0
    const initIndex = sourceList.findIndex(item=>String(item.id) === String(defaultSelected[currentFloor]?.id))
    return  initIndex<0?0:initIndex
  }
  public defaultRenderList = ()=>{
    const { currentFloor, floor, defaultSelected, sourceList,isLinkage,originData} = this.props
    if(isLinkage){
      return originData[currentFloor+1]
    }
    const getNoFefault = ()=>this.props.sourceList?.[0]?.sub || []
    const getDefaultList = ()=>{
      if(currentFloor + 1 === floor )  return []
      return sourceList.find(value=>value.id === defaultSelected[currentFloor].id)?.sub || getNoFefault()
    }
    return _.isEmpty(defaultSelected)?getNoFefault():getDefaultList()
  }
  public getDefaultChoose = ()=>{
    const { defaultSelected, currentFloor, sourceList } = this.props
    return _.isEmpty(defaultSelected)?sourceList?.[0]:(sourceList.find(item=>item.id === defaultSelected?.[currentFloor]?.id) || sourceList?.[0])
  }
  public mid = ()=>{
    const { maxLength } = this.props
    return Math.floor(maxLength/2)
  }
  public  flatListData = (nextData:any[])=>{
    const { sourceList,currentFloor,originData ,isLinkage} = this.props
    const mid = this.mid()
    const arr = Array(mid).fill({id:'',name:'',sub:[]})
    let currentData = nextData?nextData:sourceList
    if(isLinkage){
      currentData = originData[currentFloor]
    }
    if(currentData.length === 0 || !currentData) return []
    return [...arr,...currentData,...arr]
  }
  public getItemLayout = (data:any,index:any) =>{
    const { itemHeihght } = this.props
    return {length: itemHeihght, offset: itemHeihght * index, index}
  }
  public renderItem = ({item,index,separators}:any)=>{
    const { itemHeihght ,ActiveTextStyle,normalTextStyle,pickerType} = this.props
    const {currentChoose} = this.state
    const itemPress =  (pickerType === 'scroll' || item.id === currentChoose?.id)?_.noop:this.clickItem
    return <TouchableWithoutFeedback  onPress={()=>itemPress(item,index)}>
              <Text
                numberOfLines={1}
                style={[
                  {
                    height:itemHeihght,
                    textAlign:'center',
                    lineHeight:itemHeihght,
                    opacity:0.8,
                    paddingHorizontal: 10,
                  },
                  item.id === currentChoose?.id?ActiveTextStyle:normalTextStyle
                ]}
              >{item.name}</Text>
            </TouchableWithoutFeedback>
  }

  public clickItem =(item:ISourceType,index:any)=>{
    const mid = this.mid()
    if(index<mid) return
    if(!item) return
    this.scrollToIndex(index)
    this.chooseItem(index,item)
  }
  public  scrollToIndex = (index:any)=>{
    const { renderList } = this.state
    if(renderList.length === 0 || !renderList) return
    if(this.flatEl){
      this.flatEl.scrollToIndex({
        index,
        viewPosition:0.5,
        animated:true
      })
    }
  }
  public  chooseItem = (index:number,item:ISourceType)=>{
    const { setSelect,currentFloor,isLinkage } = this.props
    setSelect(item,currentFloor)
    if(isLinkage) return
    this.setState({
      currentChoose:item || {},
      nextData:item?.sub || []
    },()=>{
      if(this.itemEL) this.itemEL.reduction()
    })
  }
  public reduction=()=>{
    const currentChoose = this.props.sourceList?.[0] || {}
    const nextData = currentChoose?.sub || []
    const mid = this.mid()
    this.setState({
      currentChoose,
      nextData,
      renderList:this.flatListData(),
      surerefresh:Date.now()+Math.random(),
    })
  }
  public componentDidMount(){
    this.initSelected()
  }
  public componentDidUpdate(preProp:ISItemProps,preState:ISItemState){
    const mid = this.mid()
    if(this.state.surerefresh !== preState.surerefresh){
      _.delay(()=>this.clickItem(this.state.currentChoose,mid),100)
    }
  }
  public initSelected = ()=>{
    const {setSelect,currentFloor } = this.props
    const { currentChoose } = this.state
    setSelect(currentChoose,currentFloor)
    if(this.flatEl) this.flatEl.recordInteraction()
  }
  public renderListEmptyComponent = (data:any)=>{
    const {renderListEmptyComponent,itemHeihght,emptyOptions,maxLength} = this.props

    if(renderListEmptyComponent) return renderListEmptyComponent()
      return <View style={{height:itemHeihght*maxLength,justifyContent:'center',alignItems: 'center'}}>
        <Text
          numberOfLines={1}
          style={[
            {
              height:itemHeihght,
              textAlign:'center',
              lineHeight:itemHeihght,
              opacity:0.8,
              paddingHorizontal: 10,
            }
          ]}
        >{emptyOptions.descText || '暂无数据'}</Text>
      </View>
  }
  public render(){
    const {
      floor,
      currentFloor,
      setSelect,
      defaultSelected,
      maxLength,
      itemHeihght=50,
      ActiveTextStyle,
      normalTextStyle,
      pickerType,
      isLinkage,
      originData,
      emptyOptions,
    } = this.props
    const {
      nextData,
      initialScrollIndex,
      renderList,
    } = this.state

    const nextFloor = currentFloor + 1
    return (
      <>
        <FlatList
          style={{flex:1}}
          ref={ref=>this.flatEl = ref}
          showsHorizontalScrollIndicator={false}
          showsVerticalScrollIndicator={false}
          keyExtractor={(item: object, index: number) => String(index)}
          renderItem={this.renderItem}
          viewabilityConfig={this.config}
          data={renderList}
          getItemLayout={this.getItemLayout}
          initialScrollIndex={initialScrollIndex}
          onMomentumScrollEnd={this._moveEnd}
          scrollEnabled={pickerType === 'click'?false:true}
          ListEmptyComponent={this.renderListEmptyComponent}
        />
        {
          floor > nextFloor ?
            <PickerAlertViewItem
              ref={ref=>this.itemEL = ref}
              sourceList={nextData}
              floor={floor}
              setSelect={setSelect}
              currentFloor={nextFloor}
              defaultSelected={defaultSelected}
              maxLength={maxLength}
              itemHeihght={itemHeihght}
              ActiveTextStyle={ActiveTextStyle}
              normalTextStyle={normalTextStyle}
              pickerType={pickerType}
              isLinkage={isLinkage}
              originData={originData}
              emptyOptions={emptyOptions}
            />:
            null
        }
      </>
    )
  }
  private _moveEnd=(e)=>{
    const {itemHeihght} = this.props
    const { renderList } = this.state
    const contentOffset = e.nativeEvent.contentOffset.y;
    const mid = this.mid()
    const index = Math.round(contentOffset/itemHeihght)+mid
    this.clickItem(renderList[index],index)
  }
}

export default  WithHeadAndMethod(PickerAlertViewItem)

接上一版 多级联动组件;增加了非联动组件;完善了配置;优化了使用过程中的小问题;

Tags:

最近发表
标签列表