移动端效果之Picker
写在前面
接着前面的移动端效果的研究,这次来看看picker
选择器的实现原理
代码看这里: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))
,细细想一下就好了。 - 第二个函数的作用是获取当前
picker
的transform
值,作为下一次滑动计算的依据。其实感觉这样挺费事,因为在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度,反之下一个item
绕X
轴逆时针旋转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的更多相关文章
- 移动端效果之Swiper
写在前面 最近在做移动端方面运用到了饿了么的vue前端组件库,因为不想单纯用组件而使用它,故想深入了解一下实现原理.后续将会继续研究一下其他的组件实现原理,有兴趣的可以关注下. 代码在这里:戳我 1. ...
- 移动端效果之CellSwiper
写在前面 接着之前的移动端效果讲解,刚好项目中需要使用到这一效果,去饿了么的组件库看了一下效果,发现效果和微信端的cellSwiper还是有点差别的,由于项目中又是使用的React,之前使用的Reac ...
- 移动端效果之IndexList
写在前面 接着前面的移动端效果讲,这次讲解的的是IndexList的实现原理.效果如下: 代码请看这里:github 移动端效果之swiper 移动端效果之picker 移动端效果之cellSwipe ...
- 移动端效果之LoadMore
写在前面 列表一直是展示数据的一个重要方式,在手机端的列表展示又和PC端展示不同,毕竟手机端主要靠滑.之前手机端之前一直使用的IScroll,但是IScroll本身其实有很多兼容性BUG,想改动一下需 ...
- 移动端效果之ScrollList
写在前面 列表一直是展示数据的一个重要方式,在手机端的列表展示又和PC端展示不同,毕竟手机端主要靠滑.之前手机端之前一直使用的IScroll,但是IScroll本身其实有很多兼容性BUG,想改动一下需 ...
- flow-vue.js移动端效果
得益于vue.js和element,以及vue-element-extends在线表格编辑.前后端分离的后端用golang+beego框架,服务器采用腾讯云. vue的自适应做的很好,只要将侧栏加一行 ...
- 动端逐渐出了许多的移动端的框架,比如Sencha Touch、JQTouch、Jquery-moblie、jqMobi等等。这些框架都有优缺点,不同的框架应用在不同的项目中。现简单阐述一下各框架的优缺点:
移动前端工作的那些事---前端制作之微信小技巧篇 (2013-11-15 15:20) 转载▼ 标签: it css3/javascript html5 webapp 手机网站搭建 分类: 前端制 ...
- 使用three.js实现机器人手臂的运动效果
Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它创建各种三维场景,包括了摄影机.光影.材质等各种对象.你可以在它的主页上看到许多精彩的演示.不过,这款引擎目前还处在比较不成熟的开发阶段 ...
- 【新特性速递】FineUIPro/Mvc/Core 全新移动端访问体验(示例首页)!
移动端支持 虽然 FineUIPro 早在 2016 年就已经完成对移动端的适配工作,并新增了 50 多个官网示例. 并且,我们也新增了一个移动端的首页 http://pro.fineui.com/m ...
随机推荐
- MySQL常见的三种存储引擎(InnoDB、MyISAM、MEMORY)
简单来说,存储引擎就是指表的类型以及表在计算机上的存储方式. 存储引擎的概念是MySQL的特点,Oracle中没有专门的存储引擎的概念,Oracle有OLTP和OLAP模式的区分.不同的存储引擎决定了 ...
- sqlserver优化
有些程序员在撰写数据库应用程序时,常专注于 OOP 及各种 framework 的使用,却忽略了基本的 SQL 语句及其「性能 (performance)优化」问题.版工曾听过台湾某半导体大厂的新进程 ...
- diplay:table-cell和伪元素:after方法让图片居中
让图片居中和文字居中是不一样的,文字居中可以通过line-height等调整,让图片居中方法,参考各种资料博文和测试 目前接触两种方法 display:table-cell和伪元素:after方法 ...
- 关于 String.intern() 的思考
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt399 我看到一个问题 https://segmentfault.com/q/ ...
- 【Java学习笔记之三十三】详解Java中try,catch,finally的用法及分析
这一篇我们将会介绍java中try,catch,finally的用法 以下先给出try,catch用法: try { //需要被检测的异常代码 } catch(Exception e) { //异常处 ...
- angularjs自定义指令实现分页插件
由于最近的一个项目使用的是angularjs1.0的版本,涉及到分页查询数据的功能,后来自己就用自定义指令实现了该功能.现在单独做了个简易的小demo,主要是为了分享自己写的分页功能.注:本实例调用的 ...
- 如何在C++中产生随机数
C++中没有自带的random函数,要实现随机数的生成就需要使用rand()和srand().不过,由于rand()的内部实现是用线性同余法做的,所以生成的并不是真正的随机数,而是在一定范围内可看为随 ...
- 201521123092《java程序设计》第六周学习总结
1.本周学习总结 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 2.书面作业 1.clone方法 1.1 Object对象中的clo ...
- 201521123060 《Java程序设计》第3周学习总结
1. 本周学习总结 2. 书面作业 1.代码阅读 public class Test1 { private int i = 1;//这行不能修改 private static int j = 2; p ...
- JAVA课设--五子棋--团队博客
1 团队名称.团队成员介绍 徐璐琳 网络1511班 201521123010 祁泽文 网络1511班 201521123011 张晨晨 网络1511班 201521123009 2 项目git地址 团 ...