码迷,mamicode.com
首页 > 其他好文 > 详细

WeUI Picker组件 源代码分析

时间:2018-01-31 20:14:57      阅读:934      评论:0      收藏:0      [点我收藏+]

标签:eof   off   level   hid   led   影响   turn   https   ber   

 

前言

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

之前也做过类似的组件, 是基于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             // 没有缓存选项,而且存在defaultValue
109             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层的选中项的index
124             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回调应该在最后一个子列表滑动时再call
163                      */
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); // 不是多列的情况下才继续处理children
167                     } 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.onConfirm
183         });
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     $picker
207         .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 `<div class="weui-picker__item${item.disabled ? ‘ weui-picker__item_disabled‘ : ‘‘}">${typeof item == ‘object‘ ? item.label : item}</div>`;
 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 = $this
213         .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.js
227     const isSupportTouch = (‘ontouchstart‘ in window) || window.DocumentTouch && document instanceof window.DocumentTouch;
228     if(!isSupportTouch){
229         $this
230             .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少了一大半的体积。(weuiPicker项目地址)

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

 

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

WeUI Picker组件 源代码分析

标签:eof   off   level   hid   led   影响   turn   https   ber   

原文地址:https://www.cnblogs.com/haha1212/p/8393243.html

(0)
(1)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!