[转]Shared——RN如何实现一个ExpandableList(可展开列表)组件
作者:小石头若海
原文地址:https://segmentfault.com/a/1190000011754908
RN如何实现一个ExpandableList(可展开列表)组件
讨论与分析
首先,我们先确定下要解决的问题:
- 组件结构怎么表示?
- 展开/收起动画怎么过渡?
- 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的设计呢?因为这个组件实在太简单,感觉都编不下去了,不找个主题怎么凑字数。。。当然,这是玩笑话。实际上,在封装这个组件的时候,还是遇到了一些调用上的问题,就比如:
- 如何关联起TouchableXXX和展开/收起动画: 毫无疑问,展开/收起动画是这个组件本身就应该包掉的逻辑。但是,不同需求的groupHeader样式都是各式各样的,就比如最一开始的两个demo图。很明显,两个点击区域都不同,但是点击之后都要有展开/收起的功能,动画的同时还有不同的点击功能。或许你会想到传一个回调函数给ExpandableList,在点击GroupHeader的时候调用这个回调就好了。But,再仔细想想,别忘了TouchableXXX这一部分可是在自定义样式中的,所以ExpandableList组件中是不会包掉touch操作的,那传进来的回调到哪里去调用。。。
- 如何提高组件的性能: 上面虽然用了一个很粗浅的方法大概模拟了下组件的组成,但是很明显,用到的全是View。而既然是ExpandableList,怎么也得对得起List这个词吧。。。这可是个列表,要是数据多了,渲染性能肯定不好。因此,我们或许可以用ListView甚至FlatList来实现。不过也别忘了低版本的RN还不支持FlatList,所以需要做一个降级处理。既然这里有那么多种实现方式,那为何不暴露一个选项让用户选择ExpandableList组件到底是用哪种模式来构成。
- 展开/关闭的状态维持: 因为ExpandableList组件包掉了展开/收起动画这些操作,那组件内部势必要保存所有group的展开/收起状态。而调用ExpandableList的组件应不应该也保存一份这些展开/收起状态呢?就拿上面的仿QQ的那个demo为例,注意每个分组在展开和收起的时候,最前面的箭头样式是不一样的。所以问题就来了,groupStatus是存储在组件内部的数据,而在renderGroupHeader的时候,FriendList难道也要存储一份所有group的展开/收起状态?很显然,这种信息都是冗余的。而且一旦有两份数据,如何确保和组件内部的状态数组保持同步。这些工作无疑都不应该成为使用者的负担。
- 数据传递 这个比较简单一点,就是用户怎么知道自己点击的是第几个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)}/> );} }
稍微分析下上面的代码:
- _renderUsingView, _renderUsingListView, _renderUsingFlatList三个函数分别代表三种不同的实现方式,但是最终都调用到了_renderGroupItem。
_renderGroupItem分两个部分渲染:header和body。但是需要注意的是,在执行renderGroupHeader方法的时候,注意其中的参数。还记得文章一开始讨论的几个问题吗?status, groupId, item, toggleStatus这四个参数就能解决之前的疑惑了。
- status:当前group的展开/收起状态。通过它,我们在实现自定义GroupHeader的时候就可以知道目前的状态是什么了,从而控制不同状态下的样式展示。
- groupId:当前的group索引。
- item:当前的groupHeaderData。
- toggleStatus:这是一个方法,调用它可以控制当前group的展开/收起状态。之前讨论过touchableXXX的问题,最终可以通过它来折中实现。即调用方在使用ExpandableList组件的时候,不是要传一个renderGroupHeader属性吗,在用户实现自定义的renderGroupHeader的时候,我们把toggleStatus方法作为回调传回给renderGroupHeader。这样一来,作为组件内部就不需要关心调用方的touchableXXX是怎么样的,反正我已经把这个开关的权限交给你,你想怎么调用就怎么调用。
- 小扩展:对于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
—— 完 ——
转载于:https://www.cnblogs.com/bbcfive/p/10855335.html
[转]Shared——RN如何实现一个ExpandableList(可展开列表)组件相关推荐
- RN如何实现一个ExpandableList(可展开列表)组件
前言 今天想跟大家分享一个用RN实现的组件 - ExpandableList.恩,没什么特殊的原因,只是因为最近有一个需求要用到这东西,而且RN没有提供现成的组件,所以很(不)开(得)心(已)地做了一 ...
- 手把手教你系列 - RN如何实现一个ExpandableList(可展开列表)组件
看到一篇功能强大的博文转载下来慢慢品味 转载地址:https://blog.csdn.net/u013588817/article/details/78369331?locationNum=8& ...
- php mysql多重筛选,如何使用php、html、mysql构建一个多重分类选择列表
不适用javascript,完全通过php实现多级列表选择,列表数据从数据库获取 A very simple way to build and do a hierarchical html categ ...
- vue 循环 递归组件_Vue一个案例引发的递归组件的使用
今天我们继续使用 Vue 的撸我们的实战项目,只有在实战中我们才会领悟更多,光纸上谈兵然并卵,继上篇我们的<Vue一个案例引发的动态组件与全局事件绑定总结> 之后,今天来聊一聊我们如何在项 ...
- android 筛选控件_Flutter学习六之实现一个带筛选的列表页面
上期实现了一个网络轮播图的效果,自定义了一个轮播图组件,继承自StatefulWidget,我们知道Flutter中并没有像Android中activity的概念.页面间的跳转是通过路由从一个全屏组件 ...
- 一个页面是否应该全部组件化
一个页面是否应该全部组件化 颗粒化,我们知道通常我们组件化的时候是因为某一块功能可以复用,然后我们把它封装为组件,而对于一个界面中大部分属于业务的代码我们通常不会将他进行组件化,这个时候如果一个页面功 ...
- python怎么创建列表_用Python将一个列表分割成小列表的实例讲解 Python 如何创建一个带小数的列表...
python里有一个列表,列表里有几个小列表,小列表#冒泡排序:scoreList = [['a',98],['c',45],['b',70],['d',85],['h',85],['f',92],[ ...
- 如何封装并发布一个属于自己的ui组件库
以前就一直有个想法自己能不能封装一个类似于elementui一样的组件库,然后发布到npm上去,毕竟前端说白了,将组件v上去,然后进行数据交互.借助这次端午,终于有机会,尝试自己去封装发布组件库了 我 ...
- [react] 写一个react的高阶组件并说明你对高阶组件的理解
[react] 写一个react的高阶组件并说明你对高阶组件的理解 定义高阶组件 import React, { Component } from 'react';const simpleHoc = ...
最新文章
- MySQL的binarylog处理
- android 悬停按钮,Android悬浮按钮的使用方法
- 蓝牙扫描工具btscanner修复暴力扫描模式
- category使用 objc_setAssociatedObject/objc_getAssociatedObject 实现添加属性
- Spring 中获取servletContext及WebApplicationContext以及applicationContext三者之间的关系
- 以太网RJ45 接线标准 线序(备忘)
- 学flash就丢人吗?
- 每天Leetcode 刷题 初级算法篇-位1的个数
- centos8安装mysql_Linux宝塔面板安装
- Effective JAVA 创建和销毁对象 遇到多参构造器考虑使用构建器
- 掘金 MySQL 小册的艰辛创作历程
- Java实现pdf打印文件
- 黑客工具包ShadowBrokers浅析
- 组策略命令应用设置大全
- Flask+ZUI 开发小型工具网站(二)——ZUI
- word修改后没保存/打开了自动保存没有恢复
- linux内核编译详解
- 【Android】APP嵌入百度地图骑行导航一直初始化引擎失败解决办法
- Ubuntu18.04安装QQ、网易云音乐、百度云盘、搜狗输入法
- c语言中错误为ffblk未定义,C - 错误没有定义和存储未知
热门文章
- 有关Cantera安装在python语言环境下的历程
- css多行注释代码,css多行注释怎么写
- Latex参考文献引用出现“?”
- 充电口 米兔积木机器人_米兔积木机器人居然可以这么玩?!
- linux的scp命令用不了,CentOS使用不了scp命令怎么办
- 大顶堆小顶堆java_《排序算法》——堆排序(大顶堆,小顶堆,Java)
- 单例模式-三种实现【延迟实例化、急切实例化、内部类】
- sdl 游戏引擎c语言,kys-cpp: 《金庸群侠传》C++复刻版,这是一个以SDL2为基础实现的2D游戏引擎。同时相当于提供了一个使用该引擎制作DOS游戏《金庸群侠传》移植版的范例。...
- 工程认证计算机科学与技术专业介绍,我校计算机科学与技术专业接受全国工程教育专业认证...
- Linux grep 或者 多条件筛选