前言

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

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

项目结构

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

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

picker.js

在 picker.js 中有两个方法,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,这里只渲染了一个className
const $picker = $($.render(pickerTpl, defaults));
// depth:数据结构的深度, 多列的时候就是列数, 单列的时候是嵌套的数据的深度。
// groups:具体的滑动的列的html
let 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) {
// 没有缓存选项,而且存在defaultValue
const 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层的选中项的index
if (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

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

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; // 缓存 translate
const 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.js
const 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();
}); }
};

抽取picker

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

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

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

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

WeUI Picker组件 源代码分析的更多相关文章

  1. Android应用程序组件Content Provider的启动过程源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6963418 通过前面的学习,我们知道在Andr ...

  2. cocos2d-x 源代码分析 : control 源代码分析 ( 控制类组件 controlButton)

    源代码版本号来自3.1rc 转载请注明 cocos2d-x源代码分析总文件夹 http://blog.csdn.net/u011225840/article/details/31743129 1.继承 ...

  3. android-plugmgr源代码分析

    android-plugmgr是一个Android插件加载框架,它最大的特点就是对插件不需要进行任何约束.关于这个类库的介绍见作者博客,市面上也有一些插件加载框架,但是感觉没有这个好.在这篇文章中,我 ...

  4. 转:ffdshow 源代码分析

    ffdshow神奇的功能:视频播放时显示运动矢量和QP FFDShow可以称得上是全能的解码.编码器.最初FFDShow只是mpeg视频解码器,不过现在他能做到的远不止于此.它能够解码的视频格式已经远 ...

  5. MyBatis架构设计及源代码分析系列(一):MyBatis架构

    如果不太熟悉MyBatis使用的请先参见MyBatis官方文档,这对理解其架构设计和源码分析有很大好处. 一.概述 MyBatis并不是一个完整的ORM框架,其官方首页是这么介绍自己 The MyBa ...

  6. jqueryui.position.js源代码分析

    近期要写前端组件了.狂砍各种组件源代码,这里分析一款jqueryui中的posistion插件,注意,它不是jqueryui widget,首先看下源代码整体结构图 1.看到$.fn.position ...

  7. Hadoop源代码分析

    http://wenku.baidu.com/link?url=R-QoZXhc918qoO0BX6eXI9_uPU75whF62vFFUBIR-7c5XAYUVxDRX5Rs6QZR9hrBnUdM ...

  8. Android系统进程Zygote启动过程的源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6768304 在Android系统中,所有的应用 ...

  9. Android系统默认Home应用程序(Launcher)的启动过程源代码分析

    在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还需要有一个 Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home ...

随机推荐

  1. C# .NET锁屏程序(顺带屏蔽任务管理器)

    原文:C# .NET锁屏程序(顺带屏蔽任务管理器) 第一步:将窗体的FormBorderStyle设置为none,WindowState设为Maximized 占据整个屏幕. 第二步:使用钩子监控全局 ...

  2. 如何快速生成数据文件(fsutil命令,使用CreateFile和SetEndOfFile API函数,fopen和fseek RTL函数)

    1. fsutil 命令 文件会瞬间生成,因为实际上Windows只是分配了名称.地址和空间给该文件,并没有读写任何文件内容 100M=1024x1024x100 fsutil file create ...

  3. 起调UWP的几种方法

    原文:起调UWP的几种方法 由于种种原因吧,我需要使用一个WPF程序起调一个UWP程序,下面总结一下,给自己个备份. 启动UWP程序的关键是协议启动 给我们的UWP应用添加一个协议,like this ...

  4. SqlServer 使用脚本创建分发服务及事务复制的可更新订阅

    原文:SqlServer 使用脚本创建分发服务及事务复制的可更新订阅 [创建使用本地分发服务器] /************************[使用本地分发服务器配置发布]*********** ...

  5. Android零基础入门第4节:正确安装和配置JDK, 高富帅养成第一招

    原文:Android零基础入门第4节:正确安装和配置JDK, 高富帅养成第一招 在前几期中总结分享了Android的前世今生.Android 系统架构和应用组件那些事.带你一起来聊一聊Android开 ...

  6. shell条件测试结构

    条件测试结构 if/then结构用来判断命令列表的退出状态码是否为0(因为在UNIX惯例, 0表示"成功"), 如果成功的话, 那么就执行接下来的一个或多个命令. 有一个专有命令[ ...

  7. 进程交互还可以使用QSharedMemory

    官方例子: http://doc.qt.io/qt-5/qtcore-ipc-sharedmemory-example.html 查了一下,QSharedMemory没有自带任何信号.我的想法: 1. ...

  8. QT 线程池 + TCP 小试(三)实现最终功能

    *免分资源链接点击打开链接http://download.csdn.net/detail/goldenhawking/4492378 有了TCP.线程池,我们就可以把他们连接起来.使用最简单的 QMa ...

  9. C语言宏定义##连接符和#符的使用(MFC就是靠##自动把消息和消息函数对应起来了,借助宏来减少switch case代码的编写量)

    C语言中如何使用宏C(和C++)中的宏(Macro)属于编译器预处理的范畴,属于编译期概念(而非运行期概念).下面对常遇到的宏的使用问题做了简单总结. 关于#和## 在C语言的宏中,#的功能是将其后面 ...

  10. 下载Cloudera Repo

    wget http://archive-primary.cloudera.com/gplextras5/redhat/6/x86_64/gplextras/cloudera-gplextras5.re ...