封装灵活的React Native Grid 组件

背景

最近公司在写RN页面中,许多页面都用到了宫格类型的样式,比较典型的就是美团的这种类型。上面icon,下面title,可以左右滑动,这个任务也是交到了我手上。在经过一段时间的摸索后,也是勉勉强强的写出来了。本篇文章就带着大家一起一步一步来封装一个可用的Grid组件。 全部的代码我放到了github上:Grid

阅读本文前,你需要具有一定RN基础,官网就是最好的学习地方。另外推荐一个网站,上面有着大佬对于RN的一些思考与沉淀,十分推荐!todoit.tech/

目标

我们的目标是创建一个可复用,功能良好的Grid 组件,而目标的首要是设定好一些props来定制该组件的外观和行为。下面是我为Grid 组件设计的一些接口:

  • column:给组件定义需要多少 ,然后由 来推导。可选,不传默认按照美团的5列
  • data:数组data,里面是一个个的对象。对象包括icontitle ,和回调函数onPress必需
  • style :整个Grid组件的样式。可选
  • gridItemStyle:Grid组件中每一个item组件的样式。可选
  • iconStyle :icon(图片或者base64)的样式。可选
  • titleStyle :title(文字title)的样式。可选
  • customGird:自定义渲染Grid里面的内容。可选

补充一下:数据data 中,里面的icon或者title都是可选的。下文会给出详细的使用方法;customGrid 的类型定义为(index,item)=>React.ReactNode。在代码中会给出上述props的所有类型约束。

基础

步骤一 创建Grid父组件

tsx 复制代码
    //创建Grid.tsx的组件文件
    import {View,ViewStyle,StyleProp,StyleSheet} from 'react-native'
    import React from 'react'
    //定义类型
    interface GridProps{
        data:Data,
        column?:Column,
        style?:StyleProp<ViewStyle>
        gridItemStyle?:StyleProp<ViewStyle>,
        iconStyle?:StyleProp<ViewStyle>,
        titleStyle?:StyleProp<ViewStyle>,
        customGird?: (index:number,item: DataItem) => React.ReactNode,
    }

    type Data = DataItem[]
    
    interface DataItem{
        icon?:string,
        title?:string,
        onPress:(index:number,data:{icon?:string,title?:string})=>void,
    }

    type Column = number
    
    //创建Grid组件
    export const Grid:React.FC<GridProps> = (props)=>{
    //从props里面解构出data,style,和customGird
    const {data,style,customGird,...resetProps} = props
        
       return (
       <View style={[styles.wrapper,style]}></View>
       )
    }
    
    //创建样式
    const styles = StyleSheet.create({
        wrapper:{
            flexDirection:'row',
            flexWrap:'wrap'
        }
    })
    

从上面的代码,我们仅仅就是创建了一个叫做Gird的组件,并给这个组件给了一个叫做wrapper的样式,让他排不下了就换行和左右排列,并且我们可以通过props传入的style来改变Grid的样式,比如给Grid组件设置宽和高等等。注意:

步骤二 创建GirdItem子组件

接下来,我们应该写的是Grid中每一个Item的组件,即GridItem。然后由props中的data来决定有多少个GridItem和多少行。新增代码如下:

tsx 复制代码
//新增GridItemProps类型
interface GridItemProps{
    data:DataItem,
    index:number,
    column?:Column,
    itemStyle?:StyleProp<ViewStyle>,
    iconStyle?: StyleProp<ImageStyle>,
    titleStyle?:StyleProp<ViewStyle>,
    handleItemPress:(index:number,data:{icon?:string,title?:string})=>void
}

//新增GridItem组件
const GridItem:React.FC<GridItemProps> = (props)=>{

const {data,index,column,gridItemStyle,iconStyle,titleStyle,handleItemPress} = props
const {icon,title,onPress} = data

const width = column? Dimensions.get('window').width / column : Dimensions.get('window').width / 5
let height
    if(icon && title){
        height = 150 /2
    }else if(icon && !title){
        height = 120 /2
    }else{
        height = 80 /2
    }
// const height = icon? 150 /2 : 80 /2
    return (
        <TouchableHighlight
        activeOpacity={1}
        underlayColor='#ccc'
        key={index}
        onPress={()=>handleItemPress(index,{icon,title})}
        >
       {/* 给每一个GridItem 设置高度和宽度,使用了3个css变量 */}
            <View style={[styles.item,{width,height},gridItemStyle]}>
                {icon && <Image source={{uri:icon}} style={[styles.icon,iconStyle]}></Image>}
                {title && <Text style={[styles.title,titleStyle]}>{title}</Text>}
            </View>
        </TouchableHighlight>
    )
}

const styles = StyleSheet.create({
    item:{
        // width:Dimensions.get('window').width / 5,
        // height:150 / 2,
        justifyContent:"center",
        alignItems:'center',
    },
    icon:{
        width:76 /2,
        height:76 /2,
        marginBottom:5,
        borderRadius:5,
    },
    title:{
        height:28 /2,
        fontSize:24 /2,
        color:"#333"
    }
    })

GridItem组件中,我采用的是react native核心组件中的TouchableHighlight 作为GridItem的外壳(wrapper ),它提供了一系列的触控反馈,比如点击高亮。从而有利于用户交互。对于图片我采用的是原生组件Image ,它在处理网络和base64图片时则必须指定宽和高。对于rn核心组件的学习,请参考官网www.reactnative.cn/docs/compon... 。在上面代码中,我给每一个GridItem都设定了宽和高,Dimensions.get('window').width指的是获取当前屏幕的宽度,在父组件Grid没有传递column时,默认是5列。还有一点值得注意的是,每一个子组件点击都应该有回调函数,这里我采用的是点击???

步骤三 改造父组件Grid

子组件都写好了,父组件只有一个外壳,没有利用数据data来循环创建子组件。那怎么行。不过也不困难,只要略做修改就行。

tsx 复制代码
export const Grid:React.FC<GridProps>= (props) =>{
    const {data,style,customGird,...resetProps}  = props
    //处理每一个子组件的点击事件
    const handleItemPress = (index:number,data:{icon?:string,title:string})=>{
    console.log(inde,data,'handleItemPress')
        props.data[index].onPress(index,data)
    }
   const items = customGird?data.map((item,index)=>(
       customGird(index,item)
   )) : data.map((item,index)=>(
       //用剩余参数来传递给子组件
       <GridItem data={item} index={index} handleItemPress={handleItemPress} {...resetProps} />
   )) 
   
   return (
       <View style={[styles.wrapper,style]}>{items}</View>
   )
}

步骤三我们只做了2件事情,第一件事情 就是判断customGrid是否存在,存在的话,则使用customItem函数来渲染里面的每一个项目,否则的话就使用默认的GridItem组件。注意的是,如果你传递了customItem,那么里面的子节点全部都是完全自定义。同时,传递的(gridItemstyle,iconStyle,titleStyle,column)props就会失效。第二件事情 就是处理了每一个子组件的点击事件。我们注意到,在步骤二代码的第25行代码: onPress={()=>handleItemPress(index,{icon,title})}中,给每一个子组件的TouchableHighlight 都给了一个onPress的点击事件,意味着只要点击了每一个GirdItem组件,都会触发handleItemPress函数,并且将该子组件的index,{icon,title}参数带出来。 于是我们将hanleItemPress函数提升到父组件中,并让他触发数据data 中回调函数onPress,并携带参数。

使用案例

  1. 基础使用:
tsx 复制代码
  //数据源,假设这是后端返回的数据,这里数据太长了,我用*9代替
   const GridDemoData = [
      {
      // url or base64
      icon: 'https://yuyuss.asia/boge.png',
      title: 'bogenmsl',
      onPress: (index,{icon,title}) =>  //这是回调函数
        console.log(index,{icon,title}),
     },
     ...
     *9
     //长度为9
  ]
  <Grid data={GridDemoData} />
  

结果:

只传递data,默认5列,根据GridDemoData的长度确定行数。点击某一个GridItem,控制台会输出onPress回调函数中的index,icon和title。

  1. 只有title的案例
tsx 复制代码
    //数据源,假设这是后端返回的数据,这里数据太长了,我用*9代替
     const GridDemoData = [
        {
        //没有icon
        title: 'bogenmsl',  
        onPress: (index,{icon,title}) =>  //这是回调函数
          console.log(index,{icon,title}),
       },
       ...
       *9
       //长度为9
    ]
    <Grid data={GridDemoData} />
    

结果:

仍然只传递data,但是数据中没有icon,只有title的显示结果。

  1. 自定义案例(列为4,每一个GridItem有下和右borderWidth)
tsx 复制代码
   //数据源,假设这是后端返回的数据,这里数据太长了,我用*8代替
    const GridDemoData = [
       {
       // url or base64
       icon: 'https://yuyuss.asia/boge.png',
       title: 'bogeV50',
       onPress: (index,{icon,title}) =>  //这是回调函数
         console.log(index,{icon,title}),
      },
      ...
      *8
      //长度为8
     ]
      <Grid column={4} data={GridDemoData} gridItemstyle={{borderRightWidth:1,borderBottomWidth:1,borderColor:'#ccc'}} />

结果:

这里我们自定义了列数为4,并且给每一个GridItem都加了一个下和右边框。

扩展

到这里,我们的Grid组件就封装好了,但是距离美团的滑动滚动仍然有一点距离,我们的页面还是不能左右滑动。因此我们需要选择一个滑动组件包裹我们的Grid组件。在RN中,滑动组件有许多,比如核心组件中的ScrollViewFlatList。社区github提供的 recyclerListViewreact-native-masonry-list。他们的具体区别这里我就不一一对比了,但是底层用的都是ScrollView。这里我们选择的是ScrollView。因为图标通常来说不是很多,可以一次性可以全部加载。我们直接看代码

tsx 复制代码
const Icons = ()=>{
   const [index,setIndex] = useState(0)
   
   //获取屏幕宽度
   const maxWidth = Dimensions.get('window').width
   //处理翻页
    const handleScroll= (e:NativeSyntheticEvent<NativeScrollEvent>)=>{
        console.log('触发',e.nativeEvent.contentOffset.x,maxWidth);
        
        if(e.nativeEvent.contentOffset.x + 1 < maxWidth){
          console.log(0);
          setIndex(0)
        }else{
          console.log(1);
          setIndex(1)
        }
    }
   
   //数据源,假设这是后端返回的数据,这里数据太长了,我用*10代替
     const GridDemoData = [
        {
        // url or base64
        icon: 'https://yuyuss.asia/boge.png',
        title: 'bogeV50',
        onPress: (index,{icon,title}) =>  //这是回调函数
          console.log(index,{icon,title}),
       },
       ...
       *10
       //长度为10
      ]
      
  return (
  <View style={{marginTop:20,height:180,backgroundColor:'skyblue'}}>
          <ScrollView contentContainerStyle={styles.contentContainer} horizontal={true} pagingEnabled={true} showsHorizontalScrollIndicator={false} style={{flex:1}} onMomentumScrollEnd={(e)=>{handleScroll(e)}}
          >
            <Grid  style={{height:80,width:Dimensions.get('window').width}}   data={GridDemoData}></Grid>
            <Grid  style={{height:80,width:Dimensions.get('window').width}}  iconStyle={{borderRadius:1}} data={GridDemoData} ></Grid>
        </ScrollView>
        <View style={{flexDirection:'row',justifyContent:'center',alignItems:'center', transform:[{translateY:-5}]}}>
          <View style={index===0?styles.active:styles.gray}></View>
          <View style={index===1?styles.active:styles.gray}></View>
        </View>
  </View> 
  ) 
}

const styles = StyleSheet.create({
  contentContainer:{
    marginVertical: 12,
    // backgroundColor:'yellow',
  },
  active:{
    height:6,width:20,backgroundColor:'#ec5b45',borderRadius:5,marginRight:5
  },
  gray:{
    height:6,width:10,backgroundColor:'#a9a9a9',marginRight:5,borderRadius:2
  }
})

结果:

这里有几点需要说明:

  1. 使用ScrollView组件时,我们通常不会指定ScrollView的宽和高,而是给它的父组件设置宽和高。而且会在ScrollView的样式上加一个flex:1,使得它撑满整个父组件。而且父组件的高度应该高于Grid设置的高度*个数 , 不然高度不够,会发生截取。关于ScrollView的其他属性和方法,你应该参考官网进行学习。
  2. 这里处理翻页我只是简单的实现了一下,简单判断一下滑动的偏移量x是不是大于整个屏幕的宽度,大于的话就认为翻页,改变状态,给View不同的样式。社区github应该有更好的处理办法:React Native Tab View
相关推荐
番茄炒蛋君5 小时前
关于React-Native使用intent模式实现pda的一些思路
react native
爱lv行2 天前
Redux 在 React Native 中的使用流程
javascript·react native·react.js
爱lv行2 天前
React Native中的核心技术概念
javascript·react native·react.js
qlj2243 天前
react-native键盘遮盖底部输入框问题修复
javascript·react native·react.js
我是前端小学生7 天前
React Native 中的 View 组件:全面解析
react native
wakangda7 天前
React Native 集成 iOS 原生功能
react native·ios·cocoa
SuperHeroWu77 天前
【HarmonyOS】HarmonyOS和React Native混合开发 (一)之环境安装
react native·harmonyos·鸿蒙·开发环境·环境安装·rn·混合开发
wakangda7 天前
React Native 集成原生Android功能
javascript·react native·react.js
爱lv行12 天前
生态:React Native
javascript·react native·react.js
少恭写代码13 天前
duxapp 2024-12-18更新 新增 Svg 组件 Tab 支持幻灯片切换功能
react native·小程序·taro