写在前面

接着前面的移动端效果的研究,这次来看看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. Linux-chown命令(1)

    chown  [chang owner]:更改文件的属主,也就是指定文件的拥有者改为另一个指定的用户或组. 命令格式: chown [选项]... [用户][:[组]] 文件... 例子:  sudo ...

  2. 团队作业10——复审与事后分析(Beta版本)

    Deadline: 2017-6-13 22:00PM,以博客发表日期为准 评分基准: 按时交 - 有分,检查的项目内容为后文的两个方面 Beta阶段项目复审(单独一篇博客) 事后诸葛亮分析报告(单独 ...

  3. 201521123074 《Java程序设计》第10周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常与多线程相关内容. 2. 书面作业 本次PTA作业题集异常.多线程 Q1.finally 题目4-2 1.1 截图你的提交结果( ...

  4. 解决liblzo2.so缺失

    系统:CentOS5.8 提示错误: error while loading shared libraries: liblzo2.so.2: cannot open shared object fil ...

  5. page分页类

    <?php /** file: Page.class.php 完美分页类 Page */ class Page { private $total; //数据表中总记录数 private $lis ...

  6. 详解go语言的array和slice 【二】

    上一篇已经讲解过,array和slice的一些基本用法,使用array和slice时需要注意的地方,特别是slice需要注意的地方比较多.上一篇的最后讲解到创建新的slice时使用第三个索引来限制sl ...

  7. Java为什么把String设计成不可变的(immutable)

    在java中,String是字符串常量,可以从内存,同步机制,数据结构等方面分析 1:字符串中常量池的需要 String不同于普通基础变量类型的地方在于对象.java中的字符串对象都保存在字符串常量池 ...

  8. 利用原生js做数据管理平台

    摘要:数据管理平台在当今社会中运用十分广泛,我们在应用过程中,要对数据进行存储,管理,以及删除查询等操作,而我们在实际设计的时候,大牛们大多用到的是JQuery,而小白对jq理解也较困难,为了让大家回 ...

  9. Valgrind检测内存泄露简介

    原文地址: Valgrind 概述 体系结构 Valgrind是一套Linux下,开放源代码(GPL V2)的仿真调试工具的集合.Valgrind由内核(core)以及基于内核的其他调试工具组成.内核 ...

  10. Django(博客系统):按照时间分层筛选“/blog/article/?create_time__year=2017”,出现问题:Database returned an invalid datetime value. Are time zone definitions for your database installed?

    问题背景 添加文章时间没问题,但为了设定博客文章按照时间分层筛选(创建时间的年份.年月&月份来搜索文章),我在blog这个django app的admin.py的ArticleAdmin类中做 ...