写在前面

接着前面的移动端效果的研究,这次来看看picker选择器的实现原理

移动端效果之Swiper

移动端效果之CellSwiper

移动端效果之IndexList

代码看这里:github

1. 核心解析

1.1 基本HTML结构

<!--
说明:
1. 类 picker-3d 是为了提供3d视角,如果不需要可以去掉
2. 类 picker-slot-absolute 在3d视角中需要加上,因为下面相对定位的 picker-items 是要相对父容器进行
transform的,如果不加,就会造成位移不正确
3. DOM中所有的style样式都是在初始化的时候加上的
-->
<div class="picker picker-3d">
<div class="picker-items">
<div class="picker-slot picker-slot-absolute" style="flex:1;">
<div class="picker-slot-wrapper" id="wrapper" style="height: 108px;">
<div class="picker-item picker-selected" style="height:36px;line-height: 36px">1981</div>
<!-- ... -->
<div class="picker-item" style="height:36px;line-height: 36px">1999</div>
</div>
</div>
</div>
<div class="picker-center-highlight" style="height:36px;margin-top:-18px;"></div>
</div>

1.2 初始化DOM

由于饿了么源码中的picker是采用v-for指令生成的DOM,因此我这里只是简单的用javascript来模拟一下DOM的生成。

var el = document.querySelector('#wrapper');
var animationFrameId = null;
var currentValue;
var itemHeight = 36;
var visibleItemCount = 3;
var valueIndex = 0;
var rotateEffect = true; var datas = ['1981', '1982', '1983', '...', '1999']; // 如果支持3d视角,则给<div class="picker"></div>加上类"picker-3d"
// <div class="picker-slot" style="flex:1;">加上类"picker-slot-absolute"
if (rotateEffect) {
var picker = document.querySelector('.picker');
var pickerSlot = document.querySelector('.picker-slot');
picker.classList.add('picker-3d');
pickerSlot.classList.add('picker-slot-absolute');
} // 限定容器高度
el.style.height = `${visibleItemCount * itemHeight}px`; // 生成DOM
var html = '';
datas.forEach(function(data, index) {
html += `<div class="picker-item" style="height:36px;line-height:36px;">${data}</div>`;
});
el.innerHTML = html; // 激活当前item
var pickerItems = document.querySelectorAll('.picker-item');
pickerItems[valueIndex].classList.add('picker-selected');

1.3 初始化事件

总体上来说,picker的事件也包括滑动开始、滑动中、滑动结束。因为毕竟是移动端,滑动不可避免。这次,源码中的对滑动事件进行了封装,兼容了PC端以及排除了拖动和选择造成的影响,具体看一下分析。`

/**
* draggable.js
* 只是起到一定的兼容性
* 实质和直接调用 el.addEventListener('touchstart', startFn); 并没有多大差别
*/ // 滑动开始
// touchstart 和 mousedown 可见对PC端的兼容
// onselectstart/ondragstart 直接return 可见排除了拖动和选择
element.addEventListener(supportTouch ? 'touchstart' : 'mousedown', function(event) {
if (isDragging) return;
document.onselectstart = function() { return false; };
document.ondragstart = function() { return false; }; // ...
}); // 滑动结束
var endFn = function(event) {
// 注销事件
if (!supportTouch) {
document.removeEventListener('mousemove', moveFn);
document.removeEventListener('mouseup', endFn);
}
document.onselectstart = null;
document.ondragstart = null; isDragging = false; if (options.end) {
options.end(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
}

要是DOM跟随自己在手机屏幕上的滑动而滑动,方法大同小异,无非就是在开始滑动记录开始位置,滑动中实时计算位移,滑动结束之后将DOM滑动应该滑动的位置。这点可以参看前面一篇文章移动端效果之Swiper,这篇文章中有着相同的方法。这里重点讲一下其中的区别

// 滑动开始的执行事件方法
start: function(event) {
dragState = {
range: getDragRange(),
// ...
startTranslateTop: translateUtil.getElementTranslate(el).top
};
}

这其中有两个方法,第一个getDragRange和第二个getElementTranslate(el).

  • 第一个函数的作用是获取picker能够滑动的最小和最大的位移,这将会在滑动结束事件中用到。关于如何计算,这里简单提一下,向下滑动,最大不能超过最中间的item的最上方,这也就是为什么itemHeight * Math.floor(visibleItemCount / 2),而向上滑动,最大不能超过中间item的最下方,-itemHeight * (valuesLength - Math.ceil(visibleItemCount / 2)),细细想一下就好了。
  • 第二个函数的作用是获取当前pickertransform值,作为下一次滑动计算的依据。其实感觉这样挺费事,因为在touchend中最后肯定会计算translate值,我们只需要每次保存最后滑动的移动值就好了,而不要每次都要在DOM中取。
/**
* translateUtil
* 对浏览器对前缀支持的一些判断
* 检测浏览器对3d属性的支持情况
* 获取当前的translate值/清空picker的translate值/移动picker
* 对于浏览器的检测方面,这也算是一个比较好的工具类
*/
var docStyle = document.documentElement.style;
var engine;
var translate3d = false; // 浏览器判断
if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
engine = 'presto';
} else if ('MozAppearance' in docStyle) {
engine = 'gecko';
} else if ('WebkitAppearance' in docStyle) {
engine = 'webkit';
} else if (typeof navigator.cpuClass === 'string') {
engine = 'trident';
} // css前缀
var cssPrefix = {
trident: '-ms-', // IE
gecko: '-moz-', // FireFox
webkit: '-webkit-', // Chrome/Safari/etc...
presto: '-o-' // Opera
}[engine]; // style前缀
var vendorPrefix = {
trident: 'ms',
gecko: 'Moz',
webkit: 'Webkit',
presto: 'O'
}[engine]; var helpElem = document.createElement('div');
var perspectiveProperty = vendorPrefix + 'Perspective';
var transformProperty = vendorPrefix + 'Transform';
var transformStyleName = cssPrefix + 'transform';
var transitionProperty = vendorPrefix + 'Transition';
var transitionStyleName = cssPrefix + 'transition';
var transitionEndProperty = vendorPrefix.toLowerCase() + 'TransitionEnd'; if (helpElem.style[perspectiveProperty] !== undefined) {
translate3d = true;
} // 讲一下这个正则
// \s*(-?\d+(\.\d+?)?)px 这是一个单元,匹配这样的 -23.15px, 剩下的应该就好理解了
var regexp = /translate\(\s*(-?\d+(\.\d+?)?)px,\s*(-?\d+(\.\d+?)?)px\)\s*translateZ\(0px\)/ig;

接下来看看滑动中

drag: function(event) {
// 加上 dragging 类是为了清除过渡效果,在swiper中也有同样的应用
el.classList.add('dragging'); dragState.left = event.pageX;
dragState.top = event.pageY; var deltaY = dragState.top - dragState.startTop; // 计算当前的滑动位移
var translate = dragState.startTranslateTop + deltaY; // 滑动元素
translateUtil.translateElement(el, null, translate);
velocityTranslate = translate - prevTranslate || translate; prevTranslate = translate; if (rotateEffect) {
updateRotate(prevTranslate, pickerItems);
}
}

看到以上的代码中有一个velocityTranslate,这个值有神马作用,最开始我也不清楚,后面发现在滑动结束之后用到了,才明白了它代表了一个速率的位移值。什么是速率?就好比你快速滑动的时候,总希望它能够惯性滑动一下,这个值乘以一个惯性值就可以得出一个惯性位移。看end中的代码。

end: function() {
// 添加过渡
el.classList.remove('dragging');
// 惯性值
var momentumRatio = 7;
var currentTranslate = translateUtil.getElementTranslate(el).top;
var duration = new Date() - dragState.start; var momentumTranslate;
if (duration < 300) {
momentumTranslate = currentTranslate + velocityTranslate * momentumRatio;
} // 加上惯性速率之后的位移值
console.log('momentumTranslate', momentumTranslate); dragRange = dragState.range; setTimeout(function() {
var translate;
if (momentumTranslate) {
translate = Math.round(momentumTranslate / itemHeight) * itemHeight;
} else {
translate = Math.round(currentTranslate / itemHeight) * itemHeight;
} // 取得最终的位移值,
// 必须为itemHeight的倍数
// 在范围的最大值和最小值中取
translate = Math.max(Math.min(translate, dragRange[1]), dragRange[0]);
translateUtil.translateElement(el, null, translate); // 计算得出当前位移下应该对应的实际值
currentValue = translate2Value(translate); // 3d效果
if (rotateEffect) {
planUpdateRotate();
}
}, 10); dragState = {};
}

这就是整个picker的实现流程,撇开3d效果就可以使用了。下面看一下如何实现的3D效果。在doOnValuesChange中有一个最开始的初始化。

[].forEach.call(items, function(item, index) {
translateUtil.translateElement(item, null, itemHeight * index);
});

给每一个item设置了根据索引来的位移值,此时的每一个item的定位都必须是absolute的,这样位移下来才是紧挨着的,不然可能中间都会有一个itemHeight的空格。

3D效果中最关键的一点就是如何进行翻转角度的计算。在源码中定义了一个常量对象:

var VISIBEL_ITEMS_ANGLE_MAP = {
3: -45,
5: -20,
7: -15
};

可以看到,当只有3个可见元素的时候,高亮部分相对于X轴平行,而上一个item就必须绕X轴顺时针旋转45度,反之下一个itemX轴逆时针旋转45度。另外在其中有一段代码特别绕,根据我的理解是这样的:

// 当前item相对于顶部原本应该有的位移值
var itemOffsetTop = index * itemHeight; // 滑动过程中,相对于最开始的位置滑动的位移值
var translateOffset = dragRange[1] - currentTranslate; // 当应该有的位移值和滑动的位移值相等的时候,也就说明了当前的`item`被选中
// 也就是说此时当前的角度为0
var itemOffset = itemOffsetTop - translateOffset;
var percentage = itemOffset / itemHeight; var angle = angleUnit * percentage;
if (angle > 180) angle = 180;
if (angle < -180) angle = -180; rotateElement(item, angle);

如果觉得太绕,其实也没有必要按照他的这种做法来,我们只要想办法确定每一个item相对于当前选中的item是处于上一个还是下一个,就可以根据此来计算角度。

2. 总结

关于饿了么中的picker组件就看了这么多,整体来说跟swiper中的滑动十分相似,其中的关键点在于最后的计算位移值来根据位移值滑动到正确的位置,至于怎么计算值,其实每个人的实现方式可能都是大同小异的,也没要必要一定要按照源码来,可以适当加入自己的理解,这样或许写起代码来更加得心应手。这里只是个人的一点理解,希望能够给自己也给大家提供一点帮助。

移动端效果之Picker的更多相关文章

  1. 移动端效果之Swiper

    写在前面 最近在做移动端方面运用到了饿了么的vue前端组件库,因为不想单纯用组件而使用它,故想深入了解一下实现原理.后续将会继续研究一下其他的组件实现原理,有兴趣的可以关注下. 代码在这里:戳我 1. ...

  2. 移动端效果之CellSwiper

    写在前面 接着之前的移动端效果讲解,刚好项目中需要使用到这一效果,去饿了么的组件库看了一下效果,发现效果和微信端的cellSwiper还是有点差别的,由于项目中又是使用的React,之前使用的Reac ...

  3. 移动端效果之IndexList

    写在前面 接着前面的移动端效果讲,这次讲解的的是IndexList的实现原理.效果如下: 代码请看这里:github 移动端效果之swiper 移动端效果之picker 移动端效果之cellSwipe ...

  4. 移动端效果之LoadMore

    写在前面 列表一直是展示数据的一个重要方式,在手机端的列表展示又和PC端展示不同,毕竟手机端主要靠滑.之前手机端之前一直使用的IScroll,但是IScroll本身其实有很多兼容性BUG,想改动一下需 ...

  5. 移动端效果之ScrollList

    写在前面 列表一直是展示数据的一个重要方式,在手机端的列表展示又和PC端展示不同,毕竟手机端主要靠滑.之前手机端之前一直使用的IScroll,但是IScroll本身其实有很多兼容性BUG,想改动一下需 ...

  6. flow-vue.js移动端效果

    得益于vue.js和element,以及vue-element-extends在线表格编辑.前后端分离的后端用golang+beego框架,服务器采用腾讯云. vue的自适应做的很好,只要将侧栏加一行 ...

  7. 动端逐渐出了许多的移动端的框架,比如Sencha Touch、JQTouch、Jquery-moblie、jqMobi等等。这些框架都有优缺点,不同的框架应用在不同的项目中。现简单阐述一下各框架的优缺点:

    移动前端工作的那些事---前端制作之微信小技巧篇   (2013-11-15 15:20) 转载▼ 标签: it css3/javascript html5 webapp 手机网站搭建 分类: 前端制 ...

  8. 使用three.js实现机器人手臂的运动效果

    Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它创建各种三维场景,包括了摄影机.光影.材质等各种对象.你可以在它的主页上看到许多精彩的演示.不过,这款引擎目前还处在比较不成熟的开发阶段 ...

  9. 【新特性速递】FineUIPro/Mvc/Core 全新移动端访问体验(示例首页)!

    移动端支持 虽然 FineUIPro 早在 2016 年就已经完成对移动端的适配工作,并新增了 50 多个官网示例. 并且,我们也新增了一个移动端的首页 http://pro.fineui.com/m ...

随机推荐

  1. Myeclipse 自定义java代码快捷键

    1.首先在MyEclipse菜单栏找到"windows"下拉菜单中找到首选项(英文Prefenerces),弹出首选项界面.   2.打开java -->editor---& ...

  2. stylus选中hover元素的兄弟元素下的子元素

    stylus设置兄弟元素样式:鼠标浮动在 .video-li 元素上时,.video-li 兄弟中 .video-info 下的 .word 显示. .video-li &:hover ~ . ...

  3. 云计算的三种服务模式IaaS、PaaS和SaaS的差别

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt363 云服务"现在已经快成了一个家喻户晓的词了.如果你不知道Paa ...

  4. [转载]Web前端和后端之区分,以及面临的挑战【转】

    原文地址:Web前端和后端之区分,以及面临的挑战[转]作者:joyostyle 在我们实际的开发过程中,我们当前这样定位前端.后端开发人员. 1)前端开发人员:精通JS,能熟练应用JQuery,懂CS ...

  5. 新CCIE笔记-路由器的配置

    CCIE重修笔记之路由器基本配置与最简单的路由. 路由器与交换机的基本配置命令 全局配置模式下有多种子模式 (华为可以跳跃切换模式) 思科命令行技巧 Tab键补全,也可以直接保留缩写 问号'?'类似l ...

  6. Python并发编程协程(Coroutine)之Gevent

    Gevent官网文档地址:http://www.gevent.org/contents.html 基本概念 我们通常所说的协程Coroutine其实是corporate routine的缩写,直接翻译 ...

  7. sudoku--SE第二次作业

    git传送门 编译环境: windows10.vs2017 所用语言: c++ 首先作为一个晚上闭眼的玩家,我先来讲一下我的心路历程: 最开始接到作业的时候心里是拒绝的,刚出了一趟小远门就这样,就很难 ...

  8. 201521123076《Java程序设计》第2周学习总结

    1.本周学习总结 ①学会简单地管理原始码与位码文档(包的概念) ②了解了一些新的名词,如:完全限定(吻合)名 ③String类相关 ④in.nextInt(),in.next(),in.nextLin ...

  9. Java中 == 和 equals()详解

    java中的数据类型分为两种: 一 .基本数据类型: byte.short.int.long.float.double.char.boolean 比较它们需要用  ==  ,比较的是它们的值是否相等 ...

  10. 201521123010 《Java程序设计》第13周学习总结

    1. 本周学习总结 以你喜欢的方式(思维导图.OneNote或其他)归纳总结多网络相关内容. 2. 书面作业 1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu ...