原文链接:WeUI Picker组件 源代码分析  https://www.cnblogs.com/haha1212/p/8393243.html

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

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

项目结构:

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

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

picker.js

在picker.js中有两个方法,picker.js和datePicker。其中picker和datePicker。其中picker是核心,datePicker就是将日期数据整理好之后,再去调用picker,以下是不包含datePicker的picker注释代码。

import $ from '../util/util';//dom选择器, 在balajs上面又添加了处理dom的方法
import cron from './cron';//应用对应的日期规则,生成picker需要的数据格式
import './scroll';//滑动核心
import * as util from './util';//提供了一个获取数据嵌套深度的方法depthOf
import pickerTpl from './picker.html';//picker组件的html模版
import groupTpl from './group.html';//具体的每个滑动列表的html模版/*** 处理输入数据的每一项的结构成为 { label: item, value: item } 结构*/
function Result(item) {if(typeof item != 'object'){item = {label: item,value: item};}$.extend(this, item);
}
Result.prototype.toString = function () {return this.value;
};
Result.prototype.valueOf = function () {return this.value;
};let _sington; // 单例模式, 创建完成后为当前实例, 关闭的时候设置为false
let temp = {}; // temp 储存上一次滑动的位置function picker() {if (_sington) return _sington;//保证同时只能存在一个picker对象// 动态获取最后一个参数作为配置项const options = arguments[arguments.length - 1];// 扩展传入的配置项到默认值const defaults = $.extend({id: 'default',className: '',container: 'body',onChange: $.noop,onConfirm: $.noop,onClose: $.noop}, options);// 数据处理let items;let isMulti = false; // 是否多列的类型// 当参数大于2的时候说明是多列if (arguments.length > 2) {let i = 0;items = [];while (i < arguments.length - 1) {items.push(arguments[i++]);}isMulti = true;} else {items = arguments[0];}// 获取缓存temp[defaults.id] = temp[defaults.id] || [];// 选择结果, 会当作回调方法onChange的参数const result = [];// 根据id获取当前picker实例 选中的值的缓存, 所以声明实例的时候id要唯一const lineTemp = temp[defaults.id];// 根据模版和defaults渲染出dom,这里只渲染了一个classNameconst $picker = $($.render(pickerTpl, defaults));// depth:数据结构的深度, 多列的时候就是列数, 单列的时候是嵌套的数据的深度。// groups:具体的滑动的列的htmllet depth = options.depth || (isMulti ? items.length : util.depthOf(items[0])), groups = '';// 显示与隐藏的方法function show(){//将渲染好的pciker插入到 设置的container中, 此时每一列的内容都还没有添加进去$(defaults.container).append($picker);// 这里获取一下计算后的样式,强制触发渲染. fix IOS10下闪现的问题$.getStyle($picker[0], 'transform');// 展示组件$picker.find('.weui-mask').addClass('weui-animate-fade-in');$picker.find('.weui-picker').addClass('weui-animate-slide-up');}function _hide(callback){_hide = $.noop; // 防止二次调用导致报错// 隐藏组件$picker.find('.weui-mask').addClass('weui-animate-fade-out');$picker.find('.weui-picker').addClass('weui-animate-slide-down').on('animationend webkitAnimationEnd', function () {//动画结束后将picker移除, _sington设置为false, 执行onClose回掉, 执行hide函数传入的回掉。$picker.remove();_sington = false;defaults.onClose();callback && callback();});}function hide(callback){ _hide(callback); }/*** 初始化滚动的方法* level: 第几列或者嵌套的时候第几层* items: level对应的列的全部数据*/function scroll(items, level) {if (lineTemp[level] === undefined && defaults.defaultValue && defaults.defaultValue[level] !== undefined) {// 没有缓存选项,而且存在defaultValueconst defaultVal = defaults.defaultValue[level];let index = 0, len = items.length;// 取得默认值在items这一列中的index位置if(typeof items[index] == 'object'){for (; index < len; ++index) {if (defaultVal == items[index].value) break;}}else{for (; index < len; ++index) {if (defaultVal == items[index]) break;}}// 缓存当前实例的第level层的选中项的indexif (index < len) {lineTemp[level] = index;} else {console.warn('Picker has not match defaultValue: ' + defaultVal);}}// 寻找到第level层对应的weui-picker__group容器进行 scroll 对应的事件的绑定// scroll的具体实现放在scroll.js之中/*** items: level对应的列的全部数据* temp: level选中项的索引*/$picker.find('.weui-picker__group').eq(level).scroll({items: items,temp: lineTemp[level],onChange: function (item, index) {//为当前的result赋值。把对应的第level层选中的值放到result中if (item) {result[level] = new Result(item);} else {result[level] = null;}//更新当前实例的第level层的选中项的索引lineTemp[level] = index;if (isMulti) {// 多列的情况, 每一列都有选中的值的时候才会触发onChange回掉事件if(result.length == depth){defaults.onChange(result);}} else {/*** @子列表处理* 1. 在没有子列表,或者值列表的数组长度为0时,隐藏掉子列表。* 2. 滑动之后发现重新有子列表时,再次显示子列表。** @回调处理* 1. 因为滑动实际上是一层一层传递的:父列表滚动完成之后,会call子列表的onChange,从而带动子列表的滑动。* 2. 所以,使用者的传进来onChange回调应该在最后一个子列表滑动时再call*/if (item.children && item.children.length > 0) {$picker.find('.weui-picker__group').eq(level + 1).show();!isMulti && scroll(item.children, level + 1); // 不是多列的情况下才继续处理children} else {//如果子列表test不通过,子孙列表都隐藏。const $items = $picker.find('.weui-picker__group');$items.forEach((ele, index) => {if (index > level) {$(ele).hide();}});result.splice(level + 1);defaults.onChange(result);}}},onConfirm: defaults.onConfirm});}// 根据depth添加对应的的滑动容器个数let _depth = depth;while (_depth--) {groups += groupTpl;}// 滑动容器添加到picker组件后展示出来$picker.find('.weui-picker__bd').html(groups);show();// 展示出picker组件后根据是否是多列采用, 采用不同的机制处理// 具体都是调用 scroll 处理每一列的元素的渲染和滚动绑定if (isMulti) {items.forEach((item, index) => {scroll(item, index);});} else {scroll(items, 0);}// 给picker 绑定对应的取消和确认事件$picker.on('click', '.weui-mask', function () { hide(); }).on('click', '.weui-picker__action', function () { hide(); }).on('click', '#weui-picker-confirm', function () {defaults.onConfirm(result);});// picker的dom元素赋值给到_sington并且绑定hide函数后返回_sington = $picker[0];_sington.hide = hide;return _sington;
}

 scroll.js

import $ from '../util/util';/*** set transition* @param $target* @param time*/
const setTransition = ($target, time) => {return $target.css({'-webkit-transition': `all ${time}s`,'transition': `all ${time}s`});
};/*** set translate*/
const setTranslate = ($target, diff) => {return $target.css({'-webkit-transform': `translate3d(0, ${diff}px, 0)`,'transform': `translate3d(0, ${diff}px, 0)`});
};/*** @desc get index of middle item* @param items* @returns {number}*/
const getDefaultIndex = (items) => {let current = Math.floor(items.length / 2);let count = 0;while (!!items[current] && items[current].disabled) {current = ++current % items.length;count++;if (count > items.length) {throw new Error('No selectable item.');}}return current;
};const getDefaultTranslate = (offset, rowHeight, items) => {const currentIndex = getDefaultIndex(items);return (offset - currentIndex) * rowHeight;
};/*** get max translate* @param offset* @param rowHeight* @returns {number}*/
const getMax = (offset, rowHeight) => {return offset * rowHeight;
};/*** get min translate* @param offset* @param rowHeight* @param length* @returns {number}*/
const getMin = (offset, rowHeight, length) => {return -(rowHeight * (length - offset - 1));
};$.fn.scroll = function (options) {const defaults = $.extend({items: [],                                  // 数据scrollable: '.weui-picker__content',        // 滚动的元素offset: 3,                                  // 列表初始化时的偏移量(列表初始化时,选项是聚焦在中间的,通过offset强制往上挪3项,以达到初始选项是为顶部的那项)rowHeight: 34,                              // 列表每一行的高度onChange: $.noop,                           // onChange回调temp: null,                                 // translate的缓存bodyHeight: 7 * 34                          // picker的高度,用于辅助点击滚动的计算}, options);const items = defaults.items.map((item) => {return `<div class="weui-picker__item${item.disabled ? ' weui-picker__item_disabled' : ''}">${typeof item == 'object' ? item.label : item}</div>`;}).join('');const $this = $(this);$this.find('.weui-picker__content').html(items);let $scrollable = $this.find(defaults.scrollable);        // 可滚动的元素let start;                                                  // 保存开始按下的位置let end;                                                    // 保存结束时的位置let startTime;                                              // 开始触摸的时间let translate;                                              // 缓存 translateconst points = [];                                          // 记录移动点const windowHeight = window.innerHeight;                    // 屏幕的高度// 首次触发选中事件// 如果有缓存的选项,则用缓存的选项,否则使用中间值。if(defaults.temp !== null && defaults.temp < defaults.items.length) {const index = defaults.temp;defaults.onChange.call(this, defaults.items[index], index);translate = (defaults.offset - index) * defaults.rowHeight;}else{const index = getDefaultIndex(defaults.items);defaults.onChange.call(this, defaults.items[index], index);translate = getDefaultTranslate(defaults.offset, defaults.rowHeight, defaults.items);}//初始化的时候先根据上面代码 计算出来的 初始化 translate 运动一次setTranslate($scrollable, translate);const stop = (diff) => {//根据 计算出来的位移量diff 与 当前的偏移量translate 相加translate += diff;// 移动到最接近的那一行translate = Math.round(translate / defaults.rowHeight) * defaults.rowHeight;const max = getMax(defaults.offset, defaults.rowHeight);const min = getMin(defaults.offset, defaults.rowHeight, defaults.items.length);// 不要超过最大值或者最小值if (translate > max) {translate = max;}if (translate < min) {translate = min;}// 如果是 disabled 的就跳过let index = defaults.offset - translate / defaults.rowHeight;while (!!defaults.items[index] && defaults.items[index].disabled) {diff > 0 ? ++index : --index;}translate = (defaults.offset - index) * defaults.rowHeight;setTransition($scrollable, .3);setTranslate($scrollable, translate);// 触发选择事件defaults.onChange.call(this, defaults.items[index], index);};function _start(pageY){start = pageY;startTime = +new Date();}function _move(pageY){end = pageY;const diff = end - start;setTransition($scrollable, 0);setTranslate($scrollable, (translate + diff));startTime = +new Date();points.push({time: startTime, y: end});if (points.length > 40) {points.shift();}}function _end(pageY){if(!start) return;/*** 思路:* 0. touchstart 记录按下的点和时间* 1. touchmove 移动时记录前 40个经过的点和时间* 2. touchend 松开手时, 记录该点和时间. 如果松开手时的时间, 距离上一次 move时的时间超过 100ms, 那么认为停止了, 不执行惯性滑动*    如果间隔时间在 100ms 内, 查找 100ms 内最近的那个点, 和松开手时的那个点, 计算距离和时间差, 算出速度*    速度乘以惯性滑动的时间, 例如 300ms, 计算出应该滑动的距离*/const endTime = new Date().getTime();const relativeY = windowHeight - (defaults.bodyHeight / 2);end = pageY;// 如果上次时间距离松开手的时间超过 100ms, 则停止了, 没有惯性滑动if (endTime - startTime > 100) {//如果end和start相差小于10,则视为if (Math.abs(end - start) > 10) {stop(end - start);} else {stop(relativeY - end);}} else {if (Math.abs(end - start) > 10) {const endPos = points.length - 1;let startPos = endPos;for (let i = endPos; i > 0 && startTime - points[i].time < 100; i--) {startPos = i;}if (startPos !== endPos) {const ep = points[endPos];const sp = points[startPos];const t = ep.time - sp.time;const s = ep.y - sp.y;const v = s / t; // 出手时的速度const diff = v * 150 + (end - start); // 滑行 150ms,这里直接影响“灵敏度”stop(diff);}else {stop(0);}} else {stop(relativeY - end);}}start = null;}/*** 因为现在没有移除匿名函数的方法,所以先暴力移除(offAll),并且改变$scrollable。*/$scrollable = $this.offAll().on('touchstart', function (evt) {_start(evt.changedTouches[0].pageY);}).on('touchmove', function (evt) {_move(evt.changedTouches[0].pageY);evt.preventDefault();}).on('touchend', function (evt) {_end(evt.changedTouches[0].pageY);}).find(defaults.scrollable);// 判断是否支持touch事件 https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.jsconst isSupportTouch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch;if(!isSupportTouch){$this.on('mousedown', function(evt){_start(evt.pageY);evt.stopPropagation();evt.preventDefault();}).on('mousemove', function(evt){if(!start) return;_move(evt.pageY);evt.stopPropagation();evt.preventDefault();}).on('mouseup mouseleave', function(evt){_end(evt.pageY);evt.stopPropagation();evt.preventDefault();});}
};

参考博客: WeUI Picker组件 源代码分析  https://www.cnblogs.com/haha1212/p/8393243.html

转载:WeUI Picker组件--源码分析相关推荐

  1. Ui学习笔记---EasyUI的EasyLoader组件源码分析

    Ui学习笔记---EasyUI的EasyLoader组件源码分析 技术qq交流群:JavaDream:251572072   1.问题1:为什么只使用了dialog却加载了那么多的js   http: ...

  2. element-ui button组件 radio组件源码分析整理笔记(一)

    Button组件 button.vue <template><buttonclass="el-button"@click="handleClick&qu ...

  3. [转载]jQuery1.6.1源码分析系列

    转载:http://www.cnblogs.com/nuysoft/archive/2011/11/14/2248023.html [原创] jQuery1.6.1源码分析系列(停止更新) 作者:nu ...

  4. element-ui input组件源码分析整理笔记(六)

    input 输入框组件 源码: <template><div :class="[type === 'textarea' ? 'el-textarea' : 'el-inpu ...

  5. alibaba sentinel限流组件 源码分析

    如何使用? maven引入: <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>s ...

  6. Element UI table组件源码分析

    本文章从如下图所示的最基本的table入手,分析table组件源代码.本人已经对table组件原来的源码进行削减,源码点击这里下载.本文只对重要的代码片段进行讲解,推荐下载代码把项目运行起来,跟着文章 ...

  7. Java IO完全总结(转载) --- 重点在源码分析

    转载自https://blog.csdn.net/baobeisimple/article/details/1713797 个人认为前面对于输入输出流(超类)的 继承结构,仅仅作为参考知识了解,如果想 ...

  8. 1月24日学习内容整理:Django的admin组件源码分析及流程

    一.单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用 ...

  9. Nova组件源码分析之冷迁移与Resize

    冷迁移与Resize 1.迁移是指将虚拟机从一个计算节点迁移到另外一个节点上.冷迁移过程中虚拟机是关机或是处于不可用的状态,而热迁移则需要保证虚拟机时刻运行. 2.Resize则是指根据需求调整虚拟机 ...

最新文章

  1. 小白的 --Vuex 入门理解
  2. centos快速安装npm-2.15.8
  3. 滚动条的出现导致居中的元素会晃动
  4. 如何在freemarker寻找元素_如何让你的网站ui设计更加优秀
  5. EntityFramework Core 2.0自定义标量函数两种方式
  6. 高仿精仿手机版QQ空间应用源码
  7. netbeans7.4_NetBeans 7.4的本机Java打包
  8. win7卸载python2.7_win7重装系统后设置Python2.7环境
  9. python怎么学比较有技巧_学python必须知道的30个技巧
  10. 看图识物_有声绘本故事《晚安,建筑工地》看图识物,嘘,晚安
  11. Metro UI 的设计感悟
  12. 隐藏TreeView中SiteMap的根节点
  13. dubbo学习笔记一(服务注册)
  14. LINUX下载编译libffi
  15. arcgis api for ios
  16. 光滑的圆环(glossy torus)
  17. 求一个只包含0、1的矩阵中只包含1的最大子矩阵大小
  18. PyGame|给程序插入背景音乐
  19. 详解Javascript中正则表达式的使用
  20. 阿里云域名购买至备案流程

热门文章

  1. 利用HTML制作时间,如何使用HTML,CSS和JavaScript制作时间表?
  2. 计算机网络之HTTP状态码
  3. 游历魔法王国——网易校招
  4. imap服务器怎么填写?
  5. 哥们家大宝贝,超可爱 大家帮忙投票啊
  6. GhostMirror
  7. [实践篇]13.10 分析slog2info日志拆解qvm重启过程
  8. 软件开发有许多人都是MBTI 职业性格的ISTP类型,如果你就是这种型,恭喜你,请继续走下去...
  9. DODO和Boba Network 建立合作,提高流动性和发行能力
  10. 一级建造师(机电安装)考试系统_金桥考试虫 v2.0 下载