React Native可伸缩列表封装与快速实现方案


ReactNative.png

类似QQ好友列表的可伸缩分组列表,相信对于一部分的开发者来说还是有需要用到的,本文将会结合自己的开发理解和已发布在github和npm上的实例,结合讲解如何使用React Native快速封装此类组件。你也可以查看已经封装并发布在Github上的组件react-native-expandable-section-list查看效果,当然在你的项目中,你也可以直接使用该组件,如发现任何bug请及时提出issue给我,我会认真处理,如果你觉得组件做的还不错,请给我一个star哦!

封装思路解析


原生ios开发阶段时,UITableView功能强大,实现类似QQ列表这样的功能,你只需要在UITableView的delegate方法中作相关控制即可实现,但是最开始转到react native的时候,由于当时刚参加工作,没什么思路,后经过一些尝试和封装,才渐渐有了现在的实现方法

  • 思路一: 通过View的onLayout回调记录分组布局大小,控制打开组高度和关闭组高度实现类似开关分组效果(这个思路和代码是一个同事的方法,代码稍微有点绕,但是可以控制打开关闭的动画,我暂时放弃了使用,但是在写下一篇文章会来讲解这个方法的思路和实现)

  • 思路二: 控制数据,改变开关分组的标记值,并结合react native的state渲染实现类似开关分组效果(这是当前用在实际项目中的一个组件,下面会具体介绍如何实现它的代码和思路)

代码解析


首先渲染一个全屏的ListView,ListView中的row当作分组,row渲染成下列组件:

section.png
  • 分组渲染:
_renderRow = (rowData, sectionId, rowId) => { // eslint-disable-line
    const { renderRow, renderSectionHeaderX, renderSectionFooterX, headerKey, memberKey } = this.props;
    let memberArr = rowData[memberKey];
    if (!this.state.memberOpened.get(rowId) || !memberArr) {
      memberArr = [];
    }

    return (
      <View>
        <TouchableOpacity onPress={() => this._onPress(rowId)}>
          { renderSectionHeaderX ? renderSectionHeaderX(rowData[headerKey], rowId) : null}
        </TouchableOpacity>
        <ScrollView scrollEnabled={false}>
          {
            memberArr.map((rowItem, index) => {
              return (
                <View key={index}>
                  {renderRow ? renderRow(rowItem, index, sectionId) : null}
                </View>
              );
            })
          }
          { memberArr.length > 0 && renderSectionFooterX ? renderSectionFooterX(rowData, sectionId) : null }
        </ScrollView>
      </View>
    );
  }

说明:如代码,每个Row中都由renderSectionHeaderXrenderRowrenderSectionFooterX组成,renderSectionHeaderX通过Touchable组件的点击事件,可以控制下面的ScrollView包着的一个个组成员renderRow,及sectionFooter,每次渲染这样一个分组的时候通过当前组的开关状态this.state.memberOpened来获得当前memberArr数组是空数组或是你的传入数据rowData[memberKey],当然也可以控制你的关闭状态不一定是空数组,这里的开关状态寄存器是一个Map对象,或者你也可以自己构造合适的键值对对象

  • 开关对象构造:
 constructor(props) {
    super(props);
    this.ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
    let map = new Map();
    if (props.dataSource && props.isOpen) {
      props.dataSource.map((item, i) => map.set(i.toString(), true))
    }

    if (props.openOptions) {
      props.openOptions.map((item) => map.set(item.toString(), true))
    }
    this.state = {
      memberOpened: map
    }
  }

说明:构造状态寄存器,通过属性设置默认的分组开关状态,isOpenbool值,记录是否全部打开分组, openOptions是一个数组,里面记录你选择打开哪些分组,如打开0, 2分组,即为[0,2]

  • 点击开关方法:
_onPress = (i) => {
    this.setState((state) => {
      const memberOpened = new Map(state.memberOpened);
      memberOpened.set(i, !memberOpened.get(i)); // toggle
      return { memberOpened };
    });

    if (this.props.headerOnPress) {
      this.prop.headerOnPress(i, this.state.memberOpened.get(i) || false);
    }

    LayoutAnimation.easeInEaseOut();
  };

说明:每次点击组头,执行该方法,改变当前组在状态寄存器中设置的状态,最开始思路甚至把这状态寄存设置成为每个分组数据的某个特定属性,后来发现Map拿来这里当作一种寄存器用真的很完美。

  • 数据源
 const MockData = [
        ...
        {
            header: 'sectionHeader',
            member: [
            ...
                {
                    title: 'memberTitle',
                    content: 'content',
                },
            ...
            ]
        },
        ...
    ]

特定组件是拿来做特定用途的,react-native-expandable-section-list只适用于本文说明的情况,当然,也对数据源做一定的限制,从而快速封装出来QQ列表的可伸缩分组效果。

FlatList扩展封装


react native在版本0.43后提出使用FlatList,在使用FlatList的时候,通过同样的思路,也对FlatList做了类似的扩展,组件可见react-native-expandable-section-flatlist,0.43版本后的FlatList确实是对列表组件性能做了极大的提升,数据源data属性,加上扩展数据属性extraData的使用让每次的state组件渲染不会再是渲染当前列表,而是直接定位到你作出改变的分组从而改变分组的开关状态。keyExtractor属性也更加利于定位每一个分组的位置和设定。在数据测试时,暂只做了100个分组的测试,FlatList的性能原理是只会显示你看到的这部分分组,做的还是相当好的,暂未发现性能影响。我尽量减少了对原组件FlatList的属性影响,其他类似上拉刷新,下拉加载等属性也全部兼容,接下来直接展示精简后的代码:

class ExpanableList extends Component {
  constructor(props) {
    super(props);
    let map = new Map();
    if (props.dataSource && props.isOpen) {
      props.dataSource.map((item, i) => map.set(i, true))
    }

    if (props.openOptions) {
      props.openOptions.map((item) => map.set(item, true))
    }
    this.state = {
      memberOpened: map
    }
  }

  static propTypes = {
    dataSource: PropTypes.array.isRequired,
    headerKey: PropTypes.string,
    memberKey: PropTypes.string,
    renderRow: PropTypes.func,
    renderSectionHeaderX: PropTypes.func,
    renderSectionFooterX: PropTypes.func,
    headerOnPress: PropTypes.func,
    isOpen: PropTypes.bool,
    openOptions: PropTypes.array,
  };

  static defaultProps = {
    headerKey: 'header',
    memberKey: 'member',
    isOpen: false,
  };

  _keyExtractor = (item, index) => index;

  _onPress = (i) => {
    this.setState((state) => {
      const memberOpened = new Map(state.memberOpened);
      memberOpened.set(i, !memberOpened.get(i)); // toggle
      return { memberOpened };
    });

    if (this.props.headerOnPress) {
      this.prop.headerOnPress(i, this.state.memberOpened.get(i) || false);
    }

    LayoutAnimation.easeInEaseOut();
  };

  _renderItem = ({ item, index }) => { // eslint-disable-line
    const { renderRow, renderSectionHeaderX, renderSectionFooterX, headerKey, memberKey } = this.props;
    const sectionId = index;
    let memberArr = item[memberKey];
    if (!this.state.memberOpened.get(sectionId) || !memberArr) {
      memberArr = [];
    }

    return (
      <View>
        <TouchableOpacity onPress={() => this._onPress(sectionId)}>
          { renderSectionHeaderX ? renderSectionHeaderX(item[headerKey], sectionId) : null}
        </TouchableOpacity>
        <ScrollView scrollEnabled={false}>
          {
            memberArr.map((rowItem, rowId) => {
              return (
                <View key={rowId}>
                  {renderRow ? renderRow(rowItem, rowId, index) : null}
                </View>
              );
            })
          }
          { memberArr.length > 0 && renderSectionFooterX ? renderSectionFooterX(item, sectionId) : null }
        </ScrollView>
      </View>
    );
  };

  render() {
    const { dataSource } = this.props;
    return (
      <FlatList
        {...this.props}
        data={dataSource}
        extraData={this.state}
        keyExtractor={this._keyExtractor}
        renderItem={this._renderItem}
      />
    );
  }
}

写在最后


react-native-expandable-section-listreact-native-expandable-section-flatlist封装的相对简单,但是还是很实用的,一点小经验分享希望能对你有所帮助。

写总结和分享文章确实是一件快乐的事情,第一篇文章React Native路由理解和react-navigation库封装学习受到了一些人的肯定,真的很开心,谢谢各位,一起进步!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容