博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
WeUI Picker组件 源代码分析
阅读量:7051 次
发布时间:2019-06-28

本文共 16806 字,大约阅读时间需要 56 分钟。

 

前言

由于最近做的一个移动端项目需要使用到类似  的选择效果,  所以在这里来分析下 WeUI Picker 的实现逻辑。

之前也做过类似的组件, 是基于iscroll实现的。单列滑动的效果还可以。至于多列联动,数据结构整的太乱了, 不太好扩展。

 

1.项目结构

大家通过上面 weui.js 的项目地址去下载到本地, 打开之后找到 src 下面的 picker 就是我们今天要学习的 picker 组件的代码了。

其中picker.js 和 scroll.js 就是我们主要研究的对象。

1.1 picker.js

在 picker.js 中有两个方法,picker 和 datePicker。其中 picker 是核心, datePicker 就是将日期数据整理好之后再去调用 picker

以下是不包含 datePicker 的 picker 注释代码

1 import $ from '../util/util';//dom选择器, 在balajs上面又添加了处理dom的方法  2 import cron from './cron';//应用对应的日期规则,生成picker需要的数据格式  3 import './scroll';//滑动核心  4 import * as util from './util';//提供了一个获取数据嵌套深度的方法depthOf  5 import pickerTpl from './picker.html';//picker组件的html模版  6 import groupTpl from './group.html';//具体的每个滑动列表的html模版  7   8 /**  9  * 处理输入数据的每一项的结构成为 { label: item, value: item } 结构 10  */ 11 function Result(item) { 12     if(typeof item != 'object'){ 13         item = { 14             label: item, 15             value: item 16         }; 17     } 18     $.extend(this, item); 19 } 20 Result.prototype.toString = function () { 21     return this.value; 22 }; 23 Result.prototype.valueOf = function () { 24     return this.value; 25 }; 26  27 let _sington; // 单例模式, 创建完成后为当前实例, 关闭的时候设置为false 28 let temp = {}; // temp 储存上一次滑动的位置 29  30 function picker() { 31     if (_sington) return _sington;//保证同时只能存在一个picker对象 32  33     // 动态获取最后一个参数作为配置项 34     const options = arguments[arguments.length - 1]; 35     // 扩展传入的配置项到默认值 36     const defaults = $.extend({ 37         id: 'default', 38         className: '', 39         container: 'body', 40         onChange: $.noop, 41         onConfirm: $.noop, 42         onClose: $.noop 43     }, options); 44  45     // 数据处理 46     let items; 47     let isMulti = false; // 是否多列的类型 48     // 当参数大于2的时候说明是多列 49     if (arguments.length > 2) { 50         let i = 0; 51         items = []; 52         while (i < arguments.length - 1) { 53             items.push(arguments[i++]); 54         } 55         isMulti = true; 56     } else { 57         items = arguments[0]; 58     } 59  60     // 获取缓存 61     temp[defaults.id] = temp[defaults.id] || []; 62     // 选择结果, 会当作回调方法onChange的参数 63     const result = []; 64     // 根据id获取当前picker实例 选中的值的缓存, 所以声明实例的时候id要唯一 65     const lineTemp = temp[defaults.id]; 66     // 根据模版和defaults渲染出dom,这里只渲染了一个className 67     const $picker = $($.render(pickerTpl, defaults)); 68     // depth:数据结构的深度, 多列的时候就是列数, 单列的时候是嵌套的数据的深度。 69     // groups:具体的滑动的列的html 70     let depth = options.depth || (isMulti ? items.length : util.depthOf(items[0])), groups = ''; 71  72     // 显示与隐藏的方法 73     function show(){ 74         //将渲染好的pciker插入到 设置的container中, 此时每一列的内容都还没有添加进去 75         $(defaults.container).append($picker); 76  77         // 这里获取一下计算后的样式,强制触发渲染. fix IOS10下闪现的问题 78         $.getStyle($picker[0], 'transform'); 79  80         // 展示组件 81         $picker.find('.weui-mask').addClass('weui-animate-fade-in'); 82         $picker.find('.weui-picker').addClass('weui-animate-slide-up'); 83     } 84     function _hide(callback){ 85         _hide = $.noop; // 防止二次调用导致报错 86  87         // 隐藏组件 88         $picker.find('.weui-mask').addClass('weui-animate-fade-out'); 89         $picker.find('.weui-picker') 90             .addClass('weui-animate-slide-down') 91             .on('animationend webkitAnimationEnd', function () { 92                 //动画结束后将picker移除, _sington设置为false, 执行onClose回掉, 执行hide函数传入的回掉。 93                 $picker.remove(); 94                 _sington = false; 95                 defaults.onClose(); 96                 callback && callback(); 97             }); 98     } 99     function hide(callback){ _hide(callback); }100 101     /**102      * 初始化滚动的方法103      * level: 第几列或者嵌套的时候第几层104      * items: level对应的列的全部数据105      */106     function scroll(items, level) {107         if (lineTemp[level] === undefined && defaults.defaultValue && defaults.defaultValue[level] !== undefined) {108             // 没有缓存选项,而且存在defaultValue109             const defaultVal = defaults.defaultValue[level];110             let index = 0, len = items.length;111 112             // 取得默认值在items这一列中的index位置113             if(typeof items[index] == 'object'){114                 for (; index < len; ++index) {115                     if (defaultVal == items[index].value) break;116                 }117             }else{118                 for (; index < len; ++index) {119                     if (defaultVal == items[index]) break;120                 }121             }122 123             // 缓存当前实例的第level层的选中项的index124             if (index < len) {125                 lineTemp[level] = index;126             } else {127                 console.warn('Picker has not match defaultValue: ' + defaultVal);128             }129         }130         // 寻找到第level层对应的weui-picker__group容器进行 scroll 对应的事件的绑定131         // scroll的具体实现放在scroll.js之中132         /**133          * items: level对应的列的全部数据134          * temp: level选中项的索引135          */136         $picker.find('.weui-picker__group').eq(level).scroll({137             items: items,138             temp: lineTemp[level],139             onChange: function (item, index) {140                 //为当前的result赋值。把对应的第level层选中的值放到result中141                 if (item) {142                     result[level] = new Result(item);143                 } else {144                     result[level] = null;145                 }146                 //更新当前实例的第level层的选中项的索引147                 lineTemp[level] = index;148 149                 if (isMulti) {150                     // 多列的情况, 每一列都有选中的值的时候才会触发onChange回掉事件151                     if(result.length == depth){152                         defaults.onChange(result);153                     }154                 } else {155                     /**156                      * @子列表处理157                      * 1. 在没有子列表,或者值列表的数组长度为0时,隐藏掉子列表。158                      * 2. 滑动之后发现重新有子列表时,再次显示子列表。159                      *160                      * @回调处理161                      * 1. 因为滑动实际上是一层一层传递的:父列表滚动完成之后,会call子列表的onChange,从而带动子列表的滑动。162                      * 2. 所以,使用者的传进来onChange回调应该在最后一个子列表滑动时再call163                      */164                     if (item.children && item.children.length > 0) {165                         $picker.find('.weui-picker__group').eq(level + 1).show();166                         !isMulti && scroll(item.children, level + 1); // 不是多列的情况下才继续处理children167                     } else {168                         //如果子列表test不通过,子孙列表都隐藏。169                         const $items = $picker.find('.weui-picker__group');170                         $items.forEach((ele, index) => {171                             if (index > level) {172                                 $(ele).hide();173                             }174                         });175 176                         result.splice(level + 1);177 178                         defaults.onChange(result);179                     }180                 }181             },182             onConfirm: defaults.onConfirm183         });184     }185 186     // 根据depth添加对应的的滑动容器个数187     let _depth = depth;188     while (_depth--) {189         groups += groupTpl;190     }191     // 滑动容器添加到picker组件后展示出来192     $picker.find('.weui-picker__bd').html(groups);193     show();194 195     // 展示出picker组件后根据是否是多列采用, 采用不同的机制处理196     // 具体都是调用 scroll 处理每一列的元素的渲染和滚动绑定197     if (isMulti) {198         items.forEach((item, index) => {199             scroll(item, index);200         });201     } else {202         scroll(items, 0);203     }204 205     // 给picker 绑定对应的取消和确认事件206     $picker207         .on('click', '.weui-mask', function () { hide(); })208         .on('click', '.weui-picker__action', function () { hide(); })209         .on('click', '#weui-picker-confirm', function () {210             defaults.onConfirm(result);211         });212 213     // picker的dom元素赋值给到_sington并且绑定hide函数后返回214     _sington = $picker[0];215     _sington.hide = hide;216     return _sington;217 }
View Code

 

1.2 scroll.js

本来想给scroll.js写点注释的, 后来发现人家注释已经写的很好了,  OTZ。

1 import $ from '../util/util';  2   3 /**  4  * set transition  5  * @param $target  6  * @param time  7  */  8 const setTransition = ($target, time) => {  9     return $target.css({ 10         '-webkit-transition': `all ${time}s`, 11         'transition': `all ${time}s` 12     }); 13 }; 14  15  16 /** 17  * set translate 18  */ 19 const setTranslate = ($target, diff) => { 20     return $target.css({ 21         '-webkit-transform': `translate3d(0, ${diff}px, 0)`, 22         'transform': `translate3d(0, ${diff}px, 0)` 23     }); 24 }; 25  26 /** 27  * @desc get index of middle item 28  * @param items 29  * @returns {number} 30  */ 31 const getDefaultIndex = (items) => { 32     let current = Math.floor(items.length / 2); 33     let count = 0; 34     while (!!items[current] && items[current].disabled) { 35         current = ++current % items.length; 36         count++; 37  38         if (count > items.length) { 39             throw new Error('No selectable item.'); 40         } 41     } 42  43     return current; 44 }; 45  46 const getDefaultTranslate = (offset, rowHeight, items) => { 47     const currentIndex = getDefaultIndex(items); 48  49     return (offset - currentIndex) * rowHeight; 50 }; 51  52 /** 53  * get max translate 54  * @param offset 55  * @param rowHeight 56  * @returns {number} 57  */ 58 const getMax = (offset, rowHeight) => { 59     return offset * rowHeight; 60 }; 61  62 /** 63  * get min translate 64  * @param offset 65  * @param rowHeight 66  * @param length 67  * @returns {number} 68  */ 69 const getMin = (offset, rowHeight, length) => { 70     return -(rowHeight * (length - offset - 1)); 71 }; 72  73 $.fn.scroll = function (options) { 74     const defaults = $.extend({ 75         items: [],                                  // 数据 76         scrollable: '.weui-picker__content',        // 滚动的元素 77         offset: 3,                                  // 列表初始化时的偏移量(列表初始化时,选项是聚焦在中间的,通过offset强制往上挪3项,以达到初始选项是为顶部的那项) 78         rowHeight: 34,                              // 列表每一行的高度 79         onChange: $.noop,                           // onChange回调 80         temp: null,                                 // translate的缓存 81         bodyHeight: 7 * 34                          // picker的高度,用于辅助点击滚动的计算 82     }, options); 83     const items = defaults.items.map((item) => { 84         return `
${
typeof item == 'object' ? item.label : item}
`; 85 }).join(''); 86 const $this = $(this); 87 88 $this.find('.weui-picker__content').html(items); 89 90 let $scrollable = $this.find(defaults.scrollable); // 可滚动的元素 91 let start; // 保存开始按下的位置 92 let end; // 保存结束时的位置 93 let startTime; // 开始触摸的时间 94 let translate; // 缓存 translate 95 const points = []; // 记录移动点 96 const windowHeight = window.innerHeight; // 屏幕的高度 97 98 // 首次触发选中事件 99 // 如果有缓存的选项,则用缓存的选项,否则使用中间值。100 if(defaults.temp !== null && defaults.temp < defaults.items.length) {101 const index = defaults.temp;102 defaults.onChange.call(this, defaults.items[index], index);103 translate = (defaults.offset - index) * defaults.rowHeight;104 }else{105 const index = getDefaultIndex(defaults.items);106 defaults.onChange.call(this, defaults.items[index], index);107 translate = getDefaultTranslate(defaults.offset, defaults.rowHeight, defaults.items);108 }109 110 //初始化的时候先根据上面代码 计算出来的 初始化 translate 运动一次111 setTranslate($scrollable, translate);112 113 const stop = (diff) => {114 //根据 计算出来的位移量diff 与 当前的偏移量translate 相加115 translate += diff;116 117 // 移动到最接近的那一行118 translate = Math.round(translate / defaults.rowHeight) * defaults.rowHeight;119 const max = getMax(defaults.offset, defaults.rowHeight);120 const min = getMin(defaults.offset, defaults.rowHeight, defaults.items.length);121 // 不要超过最大值或者最小值122 if (translate > max) {123 translate = max;124 }125 if (translate < min) {126 translate = min;127 }128 129 // 如果是 disabled 的就跳过130 let index = defaults.offset - translate / defaults.rowHeight;131 while (!!defaults.items[index] && defaults.items[index].disabled) {132 diff > 0 ? ++index : --index;133 }134 translate = (defaults.offset - index) * defaults.rowHeight;135 setTransition($scrollable, .3);136 setTranslate($scrollable, translate);137 138 // 触发选择事件139 defaults.onChange.call(this, defaults.items[index], index);140 };141 142 function _start(pageY){143 start = pageY;144 startTime = +new Date();145 }146 function _move(pageY){147 end = pageY;148 const diff = end - start;149 150 setTransition($scrollable, 0);151 setTranslate($scrollable, (translate + diff));152 startTime = +new Date();153 points.push({time: startTime, y: end});154 if (points.length > 40) {155 points.shift();156 }157 }158 function _end(pageY){159 if(!start) return;160 161 /**162 * 思路:163 * 0. touchstart 记录按下的点和时间164 * 1. touchmove 移动时记录前 40个经过的点和时间165 * 2. touchend 松开手时, 记录该点和时间. 如果松开手时的时间, 距离上一次 move时的时间超过 100ms, 那么认为停止了, 不执行惯性滑动166 * 如果间隔时间在 100ms 内, 查找 100ms 内最近的那个点, 和松开手时的那个点, 计算距离和时间差, 算出速度167 * 速度乘以惯性滑动的时间, 例如 300ms, 计算出应该滑动的距离168 */169 const endTime = new Date().getTime();170 const relativeY = windowHeight - (defaults.bodyHeight / 2);171 end = pageY;172 173 // 如果上次时间距离松开手的时间超过 100ms, 则停止了, 没有惯性滑动174 if (endTime - startTime > 100) {175 //如果end和start相差小于10,则视为176 if (Math.abs(end - start) > 10) {177 stop(end - start);178 } else {179 stop(relativeY - end);180 }181 } else {182 if (Math.abs(end - start) > 10) {183 const endPos = points.length - 1;184 let startPos = endPos;185 for (let i = endPos; i > 0 && startTime - points[i].time < 100; i--) {186 startPos = i;187 }188 189 if (startPos !== endPos) {190 const ep = points[endPos];191 const sp = points[startPos];192 const t = ep.time - sp.time;193 const s = ep.y - sp.y;194 const v = s / t; // 出手时的速度195 const diff = v * 150 + (end - start); // 滑行 150ms,这里直接影响“灵敏度”196 stop(diff);197 }198 else {199 stop(0);200 }201 } else {202 stop(relativeY - end);203 }204 }205 206 start = null;207 }208 209 /**210 * 因为现在没有移除匿名函数的方法,所以先暴力移除(offAll),并且改变$scrollable。211 */212 $scrollable = $this213 .offAll()214 .on('touchstart', function (evt) {215 _start(evt.changedTouches[0].pageY);216 })217 .on('touchmove', function (evt) {218 _move(evt.changedTouches[0].pageY);219 evt.preventDefault();220 })221 .on('touchend', function (evt) {222 _end(evt.changedTouches[0].pageY);223 })224 .find(defaults.scrollable);225 226 // 判断是否支持touch事件 https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js227 const isSupportTouch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch;228 if(!isSupportTouch){229 $this230 .on('mousedown', function(evt){231 _start(evt.pageY);232 evt.stopPropagation();233 evt.preventDefault();234 })235 .on('mousemove', function(evt){236 if(!start) return;237 238 _move(evt.pageY);239 evt.stopPropagation();240 evt.preventDefault();241 })242 .on('mouseup mouseleave', function(evt){243 _end(evt.pageY);244 evt.stopPropagation();245 evt.preventDefault();246 });247 248 }249 };
View Code

 

1.3 抽取picker

研究完了, 肯定要想着怎么使用起来。

但是我们可能只想使用 picker 组件, 所以我这里把 picker 单独打包压缩了一份放到github上,  抽取之后的picker.min.js比原来的weui.min.js少了一大半的体积。

有需要的童鞋可以自取, 也可以根据weui的项目自行打包。

 

ps: 第一次写, 有不合理的地方请大家多多指正 : )

转载于:https://www.cnblogs.com/haha1212/p/8393243.html

你可能感兴趣的文章
通过Git WebHooks+脚本实现自动更新发布代码之shell脚本
查看>>
H3C设备之配置VLAN
查看>>
显示程序执行时间php函数代码
查看>>
配置远程访问服务(×××)
查看>>
大型网站架构之分布式消息队列
查看>>
junit 在MyEclipse上怎么用?
查看>>
WebKit技术内幕
查看>>
数组应用--图片切换
查看>>
Tomcat集群通过NoSQL高速存储共享session的实例
查看>>
CentOS上安装Bugzilla 4.5.2
查看>>
这辈子最有先见之明的一个设计
查看>>
vue分页
查看>>
控件拉伸(转)
查看>>
我的友情链接
查看>>
Whats new in openstack juno
查看>>
个人网站创业一年的悲惨经历分享
查看>>
学习笔记之urllib篇
查看>>
python练习-for range if continue
查看>>
7.28_Linux_ext数据结构inode的原理浅析、软硬链接的区别
查看>>
挂号网 牛B
查看>>