前言

今天想跟大家分享一个用RN实现的组件 - ExpandableList。恩,没什么特殊的原因,只是因为最近有一个需求要用到这东西,而且RN没有提供现成的组件,所以很(不)开(得)心(已)地做了一个。下面两张图是用这个组件实现的两个demo,github地址在这儿,有兴趣的可以戳https://github.com/SmallStoneSK/react-native-expandable-list瞅一眼,喜欢的还可以star一个~

如果有哪说的不对的,欢迎指出哦~



讨论与分析

好了,废话不多说,直接进入正题。首先,我们先确定下要解决的问题:

  1. 组件结构怎么表示?
  2. 展开/收起动画怎么过渡?
  3. API设计成怎样让组件的实用性更强?

1. 第一个问题

我们先将ExpandableList这个组件拆解一下,看看都有哪些部分。看下面的这张图,我们可以把一个ExpandableList看成是由一个个Group组成的,而每个Group又了包含GroupHeader和GroupBody,而其实GroupBody本身又是一个List。

分析完结构之后,思路瞬间就有了,这个结构用两个循环就可以表示出来了,就像下面这样:

<View>{data.map((groupItem, groupIndex) => {return (<View key={`group-${groupIndex}`}>{renderGroupHeader.bind(this, groupItem. groupHeaderData, groupIndex)}{groupItem.groupListData.map((listItemData, listItemIndex) => {return (<View key={`group-${groupIndex}-list-item-${listItemIndex}`}>{renderListItem.bind(this, listItemData, groupIndex, listItemIndex)}</View>);})}</View>);})}
</View>

2. 第二个问题

没错,结构是很轻易地表示出来了。但是问题来了,展开收起的这个动画过程应该怎么现实呢?我们都知道在RN中如果要实现动画,那Animated绝对是把好手。借助Animated,我们可以很精准地控制动画的实现,当然也包括这里的展开/收起动画。但是在这里,就不劳烦这尊大佛啦~因为借助LayoutAnimation,我们可以实现地更优雅(其实就是偷懒)。

在讲LayoutAnimation之前,不妨先回顾下web中的transition。为啥捏?因为个人觉得这两者就是很像,只要给定了初始状态和终止状态,那这中间的动画切换过程就不需要我们关心了。再来看这个展开/收起的动画,是不是很符合这个条件。每个group都有两种状态,即open和closed。因此,当closed时,我们设置groupBody的height为0就可以了。

3. 第三个问题

为什么要考虑API的设计呢?因为这个组件实在太简单,感觉都编不下去了,不找个主题怎么凑字数。。。当然,这是玩笑话。实际上,在封装这个组件的时候,还是遇到了一些调用上的问题,就比如:

  1. 如何关联起TouchableXXX和展开/收起动画: 毫无疑问,展开/收起动画是这个组件本身就应该包掉的逻辑。但是,不同需求的groupHeader样式都是各式各样的,就比如最一开始的两个demo图。很明显,两个点击区域都不同,但是点击之后都要有展开/收起的功能,动画的同时还有不同的点击功能。或许你会想到传一个回调函数给ExpandableList,在点击GroupHeader的时候调用这个回调就好了。But,再仔细想想,别忘了TouchableXXX这一部分可是在自定义样式中的,所以ExpandableList组件中是不会包掉touch操作的,那传进来的回调到哪里去调用。。。
  2. 如何提高组件的性能: 上面虽然用了一个很粗浅的方法大概模拟了下组件的组成,但是很明显,用到的全是View。而既然是ExpandableList,怎么也得对得起List这个词吧。。。这可是个列表,要是数据多了,渲染性能肯定不好。因此,我们或许可以用ListView甚至FlatList来实现。不过也别忘了低版本的RN还不支持FlatList,所以需要做一个降级处理。既然这里有那么多种实现方式,那为何不暴露一个选项让用户选择ExpandableList组件到底是用哪种模式来构成。
  3. 展开/关闭的状态维持: 因为ExpandableList组件包掉了展开/收起动画这些操作,那组件内部势必要保存所有group的展开/收起状态。而调用ExpandableList的组件应不应该也保存一份这些展开/收起状态呢?就拿上面的仿QQ的那个demo为例,注意每个分组在展开和收起的时候,最前面的箭头样式是不一样的。所以问题就来了,groupStatus是存储在组件内部的数据,而在renderGroupHeader的时候,FriendList难道也要存储一份所有group的展开/收起状态?很显然,这种信息都是冗余的。而且一旦有两份数据,如何确保和组件内部的状态数组保持同步。这些工作无疑都不应该成为使用者的负担。
  4. 数据传递 这个比较简单一点,就是用户怎么知道自己点击的是第几个group,以及是当前group中的第几个listItem。

这些问题在接下来的代码中都会有答案,所以请继续往下看吧。


实现

1. 先定暴露给调用方的API

我们可以先敲定一下基础的暴露出来的接口方法:

属性 值类型 解释
data Array ExpandableList的中的数据,数组中每个对象由groupHeaderData和groupListData构成
style object 作用在ExpandableList上的样式
groupStyle object 作用在每个group上的样式
groupSpacing number group之间的间隙
implementedBy string 组件实现方式,一共有'View', 'ListView', 'FlatList'三种方式可选,默认值'FlatList'
renderGroupHeader function 渲染GroupHeader的方法
renderGroupListItem function 渲染GroupListItem的方法

所以,我们可以这么调用

<ExpandableListdata={xxx}style={xxx}groupStyle={xxx}groupSpacing={xxx}implementedBy={xxx}renderGroupHeader={xxx}renderGroupListItem={xxx}/>

2. 搭骨架

import React, {Component} from 'react';
import {View,ListView,ScrollView,FlatList,LayoutAnimation
} from 'react-native';export class ExpandableList extends Component {constructor(props) {super(props);this._supportFlatList = this. _supportFlatList.bind(this);this._renderUsingView = this._renderUsingView.bind(this);this._renderUsingFlatList = this._renderUsingFlatList.bind(this);this._renderUsingListView = this._renderUsingListView.bind(this);}_supportFlatList() {return !!FlatList;}_renderUsingFlatList() {// ...}_renderUsingView() {// ...}_renderUsingListView() {// ...}render() {const strategy = {'View': this._renderUsingView,'ListView': this._renderUsingListView,'FlatList': this._supportFlatList() ? this._renderUsingFlatList : this._renderUsingListView};let {implementedBy} = this.props;if(!strategy[implementedBy]) {implementedBy = 'FlatList';}return strategy[implementedBy]();}
}

根据上面代码中的render方法可以看到,最终使用哪种方式渲染我们的ExpandableList,完全取决于implementedBy是什么,也就是把这个决定权交给调用的人。当implementedBy的值没有设置,或者是一个不合法的值的时候,我们默认就使用FlatList来实现。而且,还对FlatList进行了降级处理,如果不支持FlatList的话,就用ListView代替实现。

3. 填坑

坑一:维护所有group的open/closed状态

因为每一个group都有自身的open/closed状态,所以倒不如在state中维护一个状态数组。而且啊,考虑到假如有这么一个场景:列表在刚渲染出来的时候,有几个group是open的,有几个group是closed的。所以,我们可以这么设计:

export class ExpandableList extends Component {constructor(props) {super(props);this.state = {groupStatus: this._getInitialGroupStatus()};}_getInitialGroupStatus() {const {initialOpenGroups = [], data = []} = this.props;// true代表open, false代表closedreturn new Array(data.length).fill(false).map((item, index) => {return initialOpenGroups.indexOf(index) !== -1;});}
}

坑二:3种不同的render实现

因为不管用哪种方式去渲染,每个group的结构是相同的,所以倒不如封装一个_renderGroupItem方法,让这3种不同的render方法调用。也就是这样:

export class ExpandableList extends Component {toggleOpenStatus(index, closeOthers) {// 支持在切换自身状态的时候,同时把其他的group都关闭const newGroupStatus = this.state.groupStatus.map((status, idx) => {return idx !== index ? (closeOthers ? false : status) : !status;});this.setState({groupStatus: newGroupStatus});}_renderGroupItem(groupItem, groupId) {const status = this.state.groupStatus[groupId];const {groupHeaderData = [], groupListData = []} = groupItem;const {renderGroupHeader, renderGroupListItem, groupStyle, groupSpacing} = this.props;const groupHeader = renderGroupHeader && renderGroupHeader({status,groupId,item: groupHeaderData,toggleStatus: this.toggleGroupStatus.bind(this, groupId)});const groupBody = groupListData.length > 0 && (<ScrollView bounces={false} style={!status && {height: 0}}>{groupListData.map((listItem, index) => (<View key={`gid:${groupId}-rid:${index}`}>{renderGroupListItem && renderGroupListItem({item: listItem,rowId: index,groupId})}</View>))}</ScrollView>);return (<Viewkey={`group-${groupId}`}style={[groupStyle, groupId && groupSpacing && {marginTop: groupSpacing}]}>{groupHeader}{groupBody}</View>);}_renderFlatListItem({item, index}) {return this._renderGroupItem(item, index);}_renderListViewItem(rowData, groupId, rowId) {return this._renderGroupItem(rowData, parseInt(rowId));}_renderUsingFlatList() {const {data=[], style} = this.props;return (<FlatListdata={data}style={style}showsVerticalScrollIndicator={false}keyExtractor={(item, index) => index}renderItem={this._renderFlatListItem}/>);}_renderUsingView() {const {data = [], style} = this.props;return (<View style={style}>{data.map((item, groupId) => {return this._renderGroupItem(item, groupId);})}</View>);}_renderUsingListView() {const {data = [], style} = this.props;return (<ListViewstyle={style}showsVerticalScrollIndicator={false}renderRow={this._renderListViewItem}dataSource={new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}).cloneWithRows(data)}/>);}
}

稍微分析下上面的代码:

  1. _renderUsingView, _renderUsingListView, _renderUsingFlatList三个函数分别代表三种不同的实现方式,但是最终都调用到了_renderGroupItem。
  2. _renderGroupItem分两个部分渲染:header和body。但是需要注意的是,在执行renderGroupHeader方法的时候,注意其中的参数。还记得文章一开始讨论的几个问题吗?status, groupId, item, toggleStatus这四个参数就能解决之前的疑惑了。

    • status:当前group的展开/收起状态。通过它,我们在实现自定义GroupHeader的时候就可以知道目前的状态是什么了,从而控制不同状态下的样式展示。
    • groupId:当前的group索引。
    • item:当前的groupHeaderData。
    • toggleStatus:这是一个方法,调用它可以控制当前group的展开/收起状态。之前讨论过touchableXXX的问题,最终可以通过它来折中实现。即调用方在使用ExpandableList组件的时候,不是要传一个renderGroupHeader属性吗,在用户实现自定义的renderGroupHeader的时候,我们把toggleStatus方法作为回调传回给renderGroupHeader。这样一来,作为组件内部就不需要关心调用方的touchableXXX是怎么样的,反正我已经把这个开关的权限交给你,你想怎么调用就怎么调用。
  3. 小扩展:对于toggleOpenStatus,我们还加了一个closeOthers的可选项。支持用户在展开某一个group的同时关闭其他的group,具体实现看代码就好了,非常简单。

坑三:动画实现

前面就提到过,用LayoutAnimation来实现我们的动画将非常简单。由于在之前的代码中,我们已经通过status来控制整个groupBody的height,所以我们只要这样就可以:


export class ExpandableList extends Component {componentWillUpdate() {LayoutAnimation.easeInEaseOut();    // 也可以用LayoutAnimation.spring()}}

是的,就只需要这一行代码,列表在展开/收起的时候就不会干巴巴的了。LayoutAnimation会自动计算height,并提供一个流畅的动画。


写在最后

说实话,其实代码很简单,只是用现成的组件进行一个封装,但是要把方方面面的东西都考虑全了,还真是不容易。所以上面的代码肯定还有可以优化的地方,以及扩展更多的功能。

最后还是照惯例再贴个github的地址吧:https://github.com/SmallStoneSK/react-native-expandable-list

RN如何实现一个ExpandableList(可展开列表)组件相关推荐

  1. [转]Shared——RN如何实现一个ExpandableList(可展开列表)组件

    作者:小石头若海 原文地址:https://segmentfault.com/a/1190000011754908 RN如何实现一个ExpandableList(可展开列表)组件 讨论与分析 首先,我 ...

  2. 手把手教你系列 - RN如何实现一个ExpandableList(可展开列表)组件

    看到一篇功能强大的博文转载下来慢慢品味 转载地址:https://blog.csdn.net/u013588817/article/details/78369331?locationNum=8& ...

  3. Flutter折叠展开列表的使用

      flutter中官方其实已经封装好了折叠展开列表:ExpansionPanel,但是官方的可定制性太差,我今天主要说下如何简单实现一个自己的折叠展开列表. 状态控制   折叠展开列表,每一个父列表 ...

  4. RecycleView实现多布局可展开列表

    代码地址如下: http://www.demodashi.com/demo/13193.html 前言 在开发的时候,我们不免会遇到这么一种数据展示,该数据有以下特征: 1. 数据要以列表形式展示 2 ...

  5. 如何实现一个优秀的散列表!

    前言 假设现在有一篇很长的文档,如果希望统计文档中每个单词在文档中出现了多少次,应该怎么做呢? 很简单! 我们可以建一个HashMap,以String类型为Key,Int类型为Value: 遍历文档中 ...

  6. php mysql多重筛选,如何使用php、html、mysql构建一个多重分类选择列表

    不适用javascript,完全通过php实现多级列表选择,列表数据从数据库获取 A very simple way to build and do a hierarchical html categ ...

  7. android 筛选控件_Flutter学习六之实现一个带筛选的列表页面

    上期实现了一个网络轮播图的效果,自定义了一个轮播图组件,继承自StatefulWidget,我们知道Flutter中并没有像Android中activity的概念.页面间的跳转是通过路由从一个全屏组件 ...

  8. python怎么创建列表_用Python将一个列表分割成小列表的实例讲解 Python 如何创建一个带小数的列表...

    python里有一个列表,列表里有几个小列表,小列表#冒泡排序:scoreList = [['a',98],['c',45],['b',70],['d',85],['h',85],['f',92],[ ...

  9. 安卓学习笔记22:常用控件 - 可展开列表视图

    文章目录 零.学习目标 一.可展开列表视图概述 二.可展开列表视图继承关系图 三.教学案例 - 选择四大名著人物 (一)运行效果 (二)涉及知识点 (三)实现步骤 1.创建安卓应用[SelectCha ...

最新文章

  1. R语言ggplot2可视化移除轴标签、图例、主体等所有附属信息实战
  2. async [ə'zɪŋk] 函数
  3. 开发手记之-在Winform中为ListBox的添加选项值,并增加双击事件
  4. java 鼠标 停止工作原理,java系统级的键盘和鼠标状态
  5. getparameter的使用
  6. thinkphp5.0 使用paginate 分页后 foreach 循环体内不能处理数据的解决办法
  7. mysql 自增列 类型_MySQL--自增列学习
  8. Sql Server Management Studio 18 打开闪退问题
  9. pla3d打印材料密度_3D打印金属材料模型过程中常见问题
  10. 壕!腾讯再公布股权激励:2.97万员工 人均49万港元
  11. javascript函数执行前期变量环境初始化过程
  12. 别用Date了,Java8新特性之日期处理,现在学会也不迟!
  13. java中使用switch case报错case expressions must be constant expres
  14. Android Camera setRecordingHint函数 在部分手机上的问题。
  15. Camtasia给视频加马赛克怎么操作?
  16. python Numpy中的array函数讲解及各参数含义
  17. 第二人生的源码分析(101)脚本的初步知识
  18. 浅析企业应收账款保理融资
  19. 深入理解5G SSB协议
  20. 乘飞机选座位的胡思乱想

热门文章

  1. PYTHON实现层次分析法(AHP)指标权重的确定
  2. 矩阵的特征值与特征向量及性质及相似矩阵
  3. python调用显卡计算_Anaconda GPU计算入门指南
  4. coot怎么调用python_pymol使用笔记讲解.doc
  5. CentOS7 下 python2.7.5 安装 pip 后再安装 requests包
  6. c++入门(类和对象and继承)
  7. 时统ptp_无压缩4K超高清EFP系统中PTP精确时钟同步技术解析
  8. 【Vue教程】Vue.js推文
  9. 字体图标的网站和使用
  10. java程序暂停指令_kill命令在Java应用中使用的注意事项小结|chu