热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

前言今天想跟大家分享一个用RN实现的组件-ExpandableList。恩,没什么特殊的原因,只是因为最近有一个需求要用到这东西,而且RN

前言

今天想跟大家分享一个用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。

结构分析

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

{data.map((groupItem, groupIndex) => {return ({renderGroupHeader.bind(this, groupItem. groupHeaderData, groupIndex)}{groupItem.groupListData.map((listItemData, listItemIndex) => {return ({renderListItem.bind(this, listItemData, groupIndex, listItemIndex)});})});})}

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

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

属性值类型解释
dataArrayExpandableList的中的数据,数组中每个对象由groupHeaderData和groupListData构成
styleobject作用在ExpandableList上的样式
groupStyleobject作用在每个group上的样式
groupSpacingnumbergroup之间的间隙
implementedBystring组件实现方式,一共有'View', 'ListView', 'FlatList'三种方式可选,默认值'FlatList'
renderGroupHeaderfunction渲染GroupHeader的方法
renderGroupListItemfunction渲染GroupListItem的方法

所以,我们可以这么调用

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 && ({groupListData.map((listItem, index) => ({renderGroupListItem && renderGroupListItem({item: listItem,rowId: index,groupId})}))});return ({groupHeader}{groupBody});}_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 ( index}renderItem={this._renderFlatListItem}/>);}_renderUsingView() {const {data = [], style} = this.props;return ({data.map((item, groupId) => {return this._renderGroupItem(item, groupId);})});}_renderUsingListView() {const {data = [], style} = this.props;return ( 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



推荐阅读
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 今天就跟大家聊聊有关怎么在Android应用中实现一个换肤功能,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根 ... [详细]
  • android 触屏处理流程,android触摸事件处理流程 ? FOOKWOOD「建议收藏」
    android触屏处理流程,android触摸事件处理流程?FOOKWOOD「建议收藏」最近在工作中,经常需要处理触摸事件,但是有时候会出现一些奇怪的bug,比如有时候会检测不到A ... [详细]
  • 详解Android  自定义UI模板设计_由浅入深
    学习安卓已有一些日子,前段时间整理了不少笔记,但是发现笔记不变分享与携带。今天开始整理博客,全当是与大家分享交流与自身学习理解的过程吧。结合最近在做的一个新闻类app及学习中的问题,一点一点整理一下, ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 带添加按钮的GridView,item的删除事件
    先上图片效果;gridView无数据时显示添加按钮,有数据时,第一格显示添加按钮,后面显示数据:布局文件:addr_manage.xml<?xmlve ... [详细]
  • 微信小程序导航跟随的实现方法
    本文介绍了在微信小程序中实现导航跟随的方法。通过设置导航的position属性和绑定滚动事件,可以实现页面向下滚动到导航位置时,导航固定在页面最上方;页面向上滚动到导航位置时,导航恢复到原始位置;点击导航可以平滑跳转到相应位置。代码示例也给出了具体实现方法。 ... [详细]
  • Sleuth+zipkin链路追踪SpringCloud微服务的解决方案
    在庞大的微服务群中,随着业务扩展,微服务个数增多,系统调用链路复杂化。Sleuth+zipkin是解决SpringCloud微服务定位和追踪的方案。通过TraceId将不同服务调用的日志串联起来,实现请求链路跟踪。通过Feign调用和Request传递TraceId,将整个调用链路的服务日志归组合并,提供定位和追踪的功能。 ... [详细]
  • 今日份分享:Flutter自定义之旋转木马
    今日份分享:Flutter自定义之旋转木马-先上图,带你回到童年时光:效果分析子布局按照圆形顺序放置且平分角度子布局旋转、支持手势滑动旋转、快速滑动抬手继续旋转、自动旋转支持X轴旋 ... [详细]
  • macOS命令行创建Android模拟器
    macOS下不安装AndroidStudio使用VSCode来开发Flutter应用使用命令行创建和管理Android模拟器设备avdmanageravdmanager 是一种命令 ... [详细]
  •  项目地址https:github.comffmydreamWiCar界面做的很难看,美工方面实在不在行。重点是按钮触摸事件的处理,这里搬了RepeatListener项目代码,例 ... [详细]
author-avatar
cgy梦回秦都
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有