当前位置: 首页 > 科技观察

手机日历组件的设计与实现

时间:2023-03-19 16:47:47 科技观察

前言在大多数客户端应用中,日期的选择和操作是一个常见的功能,使用日历组件来实现这一功能往往是一种高效的解决方案。对于日历组件的设计和开发,在常见的开源项目中,通常有两种设计思路:横向切换显示,默认渲染单个月份,通过按键或左右滑动切换月份;垂直切换显示,默认呈现多个月份。月份,上下滑动切换月份;比如添加picker切换视图,添加自定义按钮,日期单选/多选,自定义文案,日期范围限制等功能,这些基本都是基于两种思路的功能扩展。在日常应用中,两种方式各有优缺点:横向切换,初始渲染节点较少,渲染性能较好;垂直切换,具有更直观的视觉体验和更好的交互操作;然而,鱼与熊掌不可兼得,交互体验与性能之间的权衡是必须始终直面的问题。随着移动设备的不断发展和移动浏览器的不断完善,用户设备的兼容性和运行效率得到了显着提高。因此本文主要介绍垂直切换实现的NutUICalendar日历组件。题目介绍今天的题目是NutUI日历组件的设计与实现。Calendar组件是NutUI的一个日历组件。用于为用户提供直观的日期选择方式,滑动切换月份,支持选择单个日期和日期范围,支持自定义日期内容等功能。今天,我们就来看看在组件开发过程中,如何一步步实现组件功能。组件设计思路对于日历组件来说,无论交互如何设计,日期时间数据的处理都是必不可少的。毕竟视图也是服务于数据信息的。本文采用的垂直切换显示方式,也意味着我们需要对节点的渲染性能做一些优化调整。因此,我们的实现思路主要有以下几点:日期数据处理,原始数据一次性初始化,可见区域节点元素分段渲染。应用虚拟列表的方法来降低节点元素的渲染成本。完善滚动事件和边界条件的处理功能,丰富Slots、Props、Events事件等,完善可扩展组件的实现原理。01基本参数要求在处理日期数据时,我们需要首先明确我们需要的基本时间输入参数,例如:日历组件可选的时间范围,当前选择的时间。通过对传入参数的分析处理,得到我们需要的数据内容,并在后续的开发过程中,完成组件内容的渲染和事件处理。这里我画了一张图方便大家更好的理解:原始日期数据:就是我们根据日期范围计算出来的原始数据当前选择日期:显示当前月份在可见范围内,需要判断选择的日期是否在范围内daterange显示范围区间:根据当前选择的日期处理,是当前需要渲染的数据范围。容器大小信息:用于计算日期滚动切换时的位移信息。02日期数据处理日期数据的计算需要多个处理过程。首先,我们需要计算传入日期范围是否存在。如果不是,则默认使用最近一年的时间范围。然后计算存在多少个月。根据月数遍历生成日期数据。在计算单个月的日期时,每个月的第一天和一周的最后一天是不同的。我们需要根据不同的周数完成上个月和下个月的日期。这样既可以省去1号起始位置偏移量的计算,又可以为功能扩展做铺垫。//获取单个月份的日期和状态constgetDaysStatus=(currMonthDays:number,dateInfo:any)=>{let{year,month}=dateInfo;返回Array.from(Array(currMonthDays),(v,k)=>{return{day:k+1,type:"curr",year,month,};});//获取上个月最后一周的天数,填上当月的空白constgetPreDaysStatus=(preCurrMonthDays:numberweekNum:number,dateInfo:any,)=>{let{year,month}=日期信息;如果(周数>=7){周数-=7;}让months=Array.from(Array(preCurrMonthDays),(v,k)=>{return{day:k+1,type:"prev",year,month,};});返回months.slice(preCurrMonthDays-weekNum);};};处理后的数据如下:03virtuallist当我们生成或加载的数据量非常大时,会造成严重的性能问题,导致视图在一段时间内变得对操作无响应。小程序中视图渲染的问题更加明显。为了解决这个问题,虚拟列表是一个很好的解决方案:相对于全量渲染数据生成的视图,可以只渲染当前可见区域(visibleviewport)的视图。当用户滚动到可见区域时,将呈现不可见区域中的视图。比如Taro中的长列表渲染(虚拟列表):当然,以上只是一个简单的应用,日历组件的构建需要在此基础上进行优化。如下图所示,monthswrapper是需要显示月份的容器。这是因为在我们的视口中会有超过一个月的时间。同时,由于单个月份包含的节点较多,经过视口边界后渲染时可能会出现空白区域,因此我们可以保留月份的部分内容,在不可见区域更改渲染节点。如上图,scrollWrapper:是一个总月高度的容器,主要用作viewport中的滚动容器;monthsWrapper:当前渲染月份的容器;viewport:当前视口范围;当scroll事件被触发时,scrollWrapper会向下或向上移动。到达边界后,monthsWrapper内部的月份信息发生变化,其整体高度也可能发生变化。通过修改monthsWrapper的transition,保证月份改变后,视口内的内容不变,更新视口外的数据。在应用虚拟列表的同时,结合目前主流的框架,将数据添加到框架的响应式数据中。该框架根据不同的数据使用diff算法或其他机制在一定程度上复用DOM节点,减少DOM节点的数量。元素的添加和删除操作。毕竟频繁增删DOM是一件比较耗性能的事情。{{month.title}}{{day.输入=='curr'?day.day:''}}今天{{startText}}{{endText}}事件处理和边界状态01事件选择在Calendar组件中,月份的切换是通过监听scroll事件实现的scrolling的使用events之所以被考虑,是因为兼容Taro转为微信小程序。touchmove事件也可以实现加载和切换交互,但是要实现滚动效果,touch事件需要频繁触发事件来修改元素位置,表现为小程序中频繁的setData,这会造成较大的性能开销,使页面卡住暂停。02边界条件事件确定后,边界条件的判断是我们需要考虑的一个问题:每个月占据的高度不一定相同。每个月包含几个星期,这些星期不一定相同。因此,每个月所占据的高度不一定相同。因此,为了准确判断当前的滚动位置信息,需要找到相似点进行判断。这里我们以单日身高作为参考值,通过单日身高计算月份的身高,进而得到单月的平均身高。滚动位置除以平均高度以获得近似电流。如下图所示:在计算高度的过程中,因为小程序的单位是rpx,而h5是rem,所以需要转换px。lettitleHeight,itemHeight;//计算单个日期的高度//转换为小程序和H5的px处理,rpx和remif(TARO_ENV==="h5"){titleHeight=46*scalePx.value+16*scalePx.值*2;itemHeight=128*scalePx.value;}else{titleHeight=Math.floor(46*scalePx.value)+Math.floor(16*scalePx.value)*2;itemHeight=Math.floor(128*scalePx.value);}monthInfo.cssHeight=titleHeight+(monthInfo.monthData.length>35?itemHeight*6:itemHeight*5);letcssScrollHeight=0;//保存月份位置信息if(state.monthsData.length>0){cssScrollHeight=state.monthsData[state.monthsData.length-1].cssScrollHeight+state.monthsData[state.monthsData.length-1].cssHeight;}monthInfo.cssScrollHeight=cssScrollHeight;当我们得到当前的平均电流后,我们就可以进行边界条件的判断了。constmothsViewScroll=(e:any)=>{constcurrentScrollTop=e.target.scrollTop;//获取平均currentletcurrent=Math.floor(currentScrollTop/state.avgHeight);if(current==0){if(currentScrollTop>=state.monthsData[current+1].cssScrollHeight){current+=1;}}elseif(current>0&¤t=state.monthsData[current+1].cssScrollHeight){current+=1;}if(currentScrollTop=state.monthsData[current+1].cssScrollHeight+state.monthsData[current+1].cssHeight){current+=1;}if(currentScrollTop结束语本文介绍了NutUI中日历组件的设计思路和实现原理,希望能给大家一些启发和思路。最后要提一下我们的NutUI组件库。长期以来,团队成员一直在努力维护NutUI。在以后的日子里,这份坚持不会放弃。我们仍然会积极维护和迭代,为有需要的同学提供技术支持,不定期发布一些相关的文章,帮助大家更好的了解和使用我们的组件库。