今天的主题是 NutUI Picker 组件的设计与实现,Picker组件是 NutUI 的一个拾取器组件,它用于显示一系列的值集合,用户能够滚动选择集合中一项,也能够支持多个系列的值集合供用户分别选择。咱们经过一张效果图,来看看组件具体实现了什么功能。css
说到 NutUI, 可能有些人还不太了解,容咱们先简单介绍一下。NutUI 是一套京东风格的移动端Vue组件库,开发和服务于移动 Web 界面的企业级前中后台产品。经过 NutUI,能够快速搭建出风格统一的页面,提高开发效率。目前已有 50+ 个组件,这些组件被普遍使用于京东的各个移动端业务中。html
接下来,咱们会经过如下几个话题,展开今天的内容:前端
当业务达到必定规模后,会遇到不少类似功能界面,每次从新开发,会影响开发效律,且这些相近的代码可能潜伏某些问题,一旦暴露,咱们须要花费不少时间去处理业务里的相同代码。若是咱们把这些相同的代码进行合理化抽离,封装组件,多处调用,咱们会发现,开发效律获得质的飞跃。vue
经过一张图来看一下的封装组件带来的好处:node
封装组件,不只可让协同开发变得高效规范,于此同时,组件化的前端开发方式也能够为后续业务扩展带来更多便利。ios
这个组件在平常业务需求中仍是比较常见的。它既能够承载简单的选项卡功能,同时也能够知足较为繁琐的日期时间选择,亦或是级联地址选择功能。基于 picker 组件的日期时间组件,咱们也有封装,有兴趣的可访问 NutUI 组件库查看。 git
从文章前言中,咱们已经大体了解 picker 组件实现了什么功能,它经过相似滚轮的三维旋转来实现选中选择集的某一项。github
先来看看组件源码的目录结构:npm
咱们主要围绕最后三个文件来讲。
基于就近原则,咱们把相关的文件放在同一个目录下,基于职责单一原则,咱们把组件颗粒化,以保证组件尽量简单和通用性比较好。把picker组件分为父组件 picker.vue 和子组件 picker-slot.vue,子组件只负责滚轮交互处理。父组件负责处理业务类逻辑。bash
一、先来看一下dom部分的分工
<div class="nut-picker-list"> <div class="nut-picker-roller" ref="roller"> <div class="nut-picker-roller-item" :class="{'nut-picker-roller-item-hidden': isHidden(index + 1)}" v-for="(item,index) in listData" :style="setRollerStyle(index + 1)" :key="item.label" > {{item.value}} </div> </div> <div class="nut-picker-content"> <div class="nut-picker-list-panel" ref="list"> <div class="nut-picker-item" v-for="(item,index) in listData" :key="item.label " > {{item.value }} </div> </div> </div> <div class="nut-picker-indicator"></div> </div>
不想看代码?“小二,上图!”
二、css部分
把nut-picker-indicator设置在最高层级,以避免被遮盖
.nut-picker-indicator{ ... z-index: 3; }
nut-picker-roller滚轮区域
.nut-picker-roller{ z-index: 1; transform-style: preserve-3d; ... .nut-picker-roller-item{ backface-visibility: hidden; position: absolute; top: 0; ... } }
要实现一些3D效果,transform-style:preserve-3d;
是必不可少的,通常而言,该属性应用在 3D 变换的父元素上,也就是舞台元素。这样子元素就具备 3D 属性效果。在 CSS 的 3D 世界中,默认状况下,咱们能够看到背后的元素,为了切合实际,咱们经常让后面的元素不可见,因此设置子元素 backface-visibility: hidden;
值得注意的是,设置了 transform-style:preserve-3d
该属性,就不能防止子级元素溢出,若是设置了overflow:hidden
,那么transform-style:preserve-3d
将会无效。
咱们经过模拟滚轮旋转来实现组件的交互效果,用一张侧面图来更直观的看一下。
接下来咱们来看一下如何实现。
首先,须要模拟一个球体,设置选择集的每一项(如下简称“滚轮项”)为 position:absolute
,共用同一个中心点即球心,而后依次堆叠于此。
咱们先温习一些基础知识,translate3d()
函数可使一个元素在三维空间移动。这种变形的特色是,使用三维向量的坐标定义元素在每一个方向移动多少。当z轴值越大时,元素也离观看者更近,咱们经过设置z轴让滚轮项的两端到达球体表面,z轴的大小,至关于球体的半径,由于咱们设定可视区域的高度为260,因此设置半径为104,若是半径太小,咱们须要戴着高倍放大镜来寻找滚轮项,若是半径过大,那么滚轮项就跑到咱们脑后去了...,不能让眼睛长在后脑勺这么可怕的事情发生!所谓距离产生美,因此保持适当的距离(80%)是最美的。
setRollerStyle(index) { return `translate3d(0px, 0px, 104px)`; }
这时候,咱们发现,全部滚轮项从集体堆叠球心变为堆叠到球体某两个点上,咱们须要把它们按照周长平铺开来。这时,咱们要用到rotate3d()
属性,咱们滚轮是围绕 X 轴旋转,因此设定 X 轴 rotate3d(1, 0, 0, a)
便可, a 是一个角度值,用来指定元素在 3D 空间旋转的角度,值为正值,元素顺时针旋转,反之元素逆时针旋转。那这个角度如何来设定呢,能够经过一个圆心角公式来推断,圆心角的度数等于它所对的弧的度数,咱们的半径是104,弧长是36(咱们预先设定的显示区),从而四舍五入计算 a 角度为20。是否是有一种被说蒙圈的感受,咱们经过一张图,更直观的理解一下。
利用上面的分析,咱们来动态设置滚轮项的最终位置。
setRollerStyle(index) { return `transform: rotate3d(1, 0, 0, ${-this.rotation * index}deg) translate3d(0px, 0px, 104px)`; }
须要注意的是滚轮项的个数可能会不少,超过一圈的可能性是大大存在的,但咱们既不能一刀切只给用户展现指定的个数,也不能所有展现形成重叠问题出现。这时候,咱们须要把超出的部分隐藏掉,咱们知道角度值 a 是20度,圆的一周是360度,因此最多能够显示18个,咱们以当前中心为基础点,前面展现8个,后面展现9个。
isHidden(index) { return (index >= this.currIndex + 9 || index <= this.currIndex - 8) ? true : false; }
三、添加事件
最后,咱们来添加滑动事件,先获取 Vue 实例关联的 DOM 元素,设置touchstart
、touchmove
、touchend
事件,须要注意的是,咱们要记得在beforeDestroy
事件中销毁这些事件。
touchstart
事件用来记录开始点,touchmove
和touchend
事件用来记录滚动结束点,计算差值,动态设置滚轮最外层元素的滚动距离和滚动角度。在滚动时候须要对滚动距离进行修正,保证滚动的最后距离为 lineSpacing (滚轮项的高度36)的倍数值。
咱们还增长了增长弹性效果,容许touchmove
滚动超出滚动范围,而后在touchend
事件中修正位置为首项、尾项。
来看一下具体实现。
setMove(move, type, time) { let updateMove = move + this.transformY; if (type === 'end') { // touchend 滚动处理 // 超出限定滚动距离修正 if (updateMove > 0) { updateMove = 0; } if (updateMove < -(this.listData.length - 1) * this.lineSpacing) { updateMove = -(this.listData.length - 1) * this.lineSpacing; } // 设置滚动距离为lineSpacing的倍数值 let endMove = Math.round(updateMove / this.lineSpacing) * this.lineSpacing; let deg = `${(Math.abs(Math.round(endMove / this.lineSpacing)) + 1) * this.rotation}deg`; this.setTransform(endMove, type, time, deg); this.timer = setTimeout(() => { this.setChooseValue(endMove); }, time / 2); this.currIndex = (Math.abs(Math.round(endMove/ this.lineSpacing)) + 1); } else { // touchmove 滚动处理 let deg = '0deg'; if (updateMove < 0) { deg = `${(Math.abs(updateMove / this.lineSpacing) + 1) * this.rotation}deg`; } else { deg = `${((-updateMove / this.lineSpacing) + 1) * this.rotation}deg`; } this.setTransform(updateMove, null, null, deg); this.currIndex = (Math.abs(Math.round(updateMove/ this.lineSpacing)) + 1); } },
在touchend
中,为滚轮父元素增长了过渡的“缓动函数”, 模拟惯性滚动效果。
setTransform(translateY = 0, type, time = 1000, deg) { this.$refs.roller.style.transition = type === 'end' ? `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)` : ''; this.$refs.roller.style.transform = `rotate3d(1, 0, 0, ${deg})`; }
经过以上的内容,咱们的滚轮效果已经基本成型。可是咱们还想要相似 ios 上时间选择器高亮当前区域的效果,该如何实现呢?
咱们尝试了以下三种方法。
第一种,考虑当滚轮项停留在高亮选中区域的时候,字体进行变化,但实践发现,只能在滚动结束的时候让字体变化,没法在滚动过程当中设置,体验并不友好。
第二种,是否能够巧用 CSS,利用背景渐变和 background-size 配合完成渐变,利用蒙层来实现呢!
.nut-picker-mask{ position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: linear-gradient(180deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6)),linear-gradient(0deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6)); background-position: top, bottom; background-size: 100% 108px; background-repeat: no-repeat; z-index: 3; }
这里把背景设置成黄色,便于咱们看效果。
感受还能够,这样就搞定了吗?
咱们在pc端模拟一切正常,在真机上却出现了诡异的画面,上滑弹出的时候,蒙层会延迟展现,影响体验效果。只有禁止上滑过渡效果,才能够正常展现。去除上滑效果是不可能的,咱们只能考虑一下其余办法。
第三种,是否能够设置一个附属滚动,也就是上面说的高亮显示区,将其盖在滚轮上面,里面每一个元素高度等于可视区高度,当滚轮滑动的时候,高亮显示区内部列表元素跟随一块儿滑动。
实践证实,这种方法能够避免上述两个方法的弊端,完美解决咱们的需求。来看一下具体实现方法。
.nut-picker-content { position: absolute; height: 36px; ... .nut-picker-roller-item{ height: 36px; ... } }
而后在上面的 setTransform 函数中,增长高亮展现区滚动效果。
setTransform(translateY = 0, type, time = 1000, deg) { ... this.$refs.list.style.transition = type === 'end' ? `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)` : ''; this.$refs.list.style.transform = `translate3d(0, ${translateY}px, 0)`; }
除了滚动效果,咱们还有一些灰色蒙层、上滑弹出、工做栏等业务内容,咱们交由父组件去处理。咱们业务中也会涉及到多列状况,因此父组件能够把 props 数据拆分传给子组件,让每一个子组件相互独立,监听子组件event事件,传递给外层。
咱们的组件是基于px来实现的,在 issues 中,收集到部分用户遇到一些问题,这里提供了解决方案。
一、使用px2rem,滚轮旋转出现误差
由于 px 转 rem 有时候转出来的值会有误差,而且出现多个小数位,致使滚动的高度和实际转化的高度出现误差,咱们可体经过如下配置解决
第一种:在.postcssrc.js配置文件中,把nutui开头的过滤掉
module.exports = ({ file }) => { return { plugins: [ ... pxtorem({ rootValue: rootValue, propList: ['*'], minPixelValue: 2, selectorBlackList: ['.nut'] // 设置 }) } }
第二种: postcss-px2rem-exclude代替postcss-px2rem
npm uninstall postcss-px2rem npm i postcss-px2rem-exclude -D
// 在.postcssrc.js配置 module.exports = ({ file }) => { return { plugins: [ ... pxtorem({ remUnit: rootValue, exclude: '/node_modules/@nutui/nutui/packages/picker' }) ] } }
二、使用lib-flexible,组件被缩小问题
咱们的 css 是基于 data-dpr 为1的时候编写的,若是使用了 lib-flexible, 页面要设置
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
后续咱们也会考虑从代码层面上去解决上述问题。
以上就是本文的所有内容,主要介绍了 Picker 组件的一些设计思想与实现原理,若是您对这个组件感兴趣,不妨查看和试用一下,使用上有任何问题,可在 issues 上进行提问,咱们会尽快解答和修复,后续咱们也会对组件进行持续优化迭代,访问 NutUI组件库,更多组件等你发现。