PC和移动端之间的交互方式有诸多不一样的地方,本文会探讨如何解决这种交互上的过渡。

一、从鼠标到手指

PC网页的交互大多是用鼠标来完成的,到了移动端,在触摸屏上我们任性地挥舞着手指。此时对于同一套响应式的代码,就要考虑如何让交互也是响应式的,即面对不同的设备提供不同的交互(操作)。

1.1、 hover

在移动端并非完全不支持css中的:hover伪类,只不过表现效果与PC不同。在移动端的表现一般会有两种情况,一种是触摸这个元素,相应的:hover效果显示,触摸这个元素之外的地方:hover效果消失;另一种是手指触摸到这个元素,相应的:hover效果显示,手指离开拿起,离开这个元素,:hover效果立即(或延时)消失。
有时候:hover在移动端的这种效果并不是我们想要的,所以我们要在移动设备上屏蔽它。我们可以用modernizr.js来检测设备是否支持touch事件,modernizr会把相应检测结果的类名加到html元素上,所以样式可能是这样的:

.no-touch .targetEle:hover{
/* hover后的显示效果 */
}

这条样式只有在不支持触摸的设备上才会生效,那么对于智能手机等可触摸设备就不会有任何影响。
那如何让上面代码中的.targetEle在移动端正常显示呢?不防先用click来做个toggle效果。为了不在PC上与:hover冲突,代码可能是这样的:

$('.touch .targetEle').on('click',function(evt){
//$(this).toggle(fun1,fun2);
});

选择器中的.touch类就避免了与PC上:hover的冲突。

1.2、click

当我们在移动端使用click事件时,会发现在一个经典的300毫秒延迟问题。即手指离开屏幕后会等个300毫秒左右才会触发click事件。这是一个历史遗留问题,原因是iphone最初想检测用户是不是要通过双击屏幕来将页面放大浏览,所以300毫秒是在等待第二次点击以确定double click事件。显然,我们在最开始的时候已经对整个页面禁用了缩放:


所以,这种延时等待对于响应式的网站似乎有些多余。所以人们想方设法想要去掉这300毫秒的等待。不同的库都为我们提供了现成的tap事件(注:tap事件并非DOM原生事件),即使库本身没有提供也有插件实现。但是核心的思路都是用touchstart事件和touchend事件(注:touchstart和touchend是DOM原生事件)结合,通过时间和坐标比较来确定是否触发tap事件。
如果你使用Zepto,可以引入它的touch.js模块,就可以直接使用tap等触摸事件。touch模块一加载,zepto就会监听整个document的触摸事件(包括touchstart、touchmove、touchend和只有Windows设备才支持的一些pointer事件),然后进行空间和坐标的判断,在touch事件的target对象上触发tap事件,其实内部做了一个监听函数的回调。这种方法看上去似乎有些低效,因为前提是你并不知道哪个元素注册了tap事件,如果一个元素嵌套太深,只要满足tap触发条件的,其所有祖先元素(包括document)都会被触发tap事件,而不管这些元素到底有没有监听tap事件。
如果你使用jQuery,可以使用它的jTap插件。jQuery有自己独立的事件系统,它预留了自定义事件接口:

jQuery.event.special['tap'] = {
/**
+ 初始化事件处理器 - this指向DOM元素
+ @data 附加的数据
+ @namespaces 事件类型命名空间
+ @eventHandle 回调函数
*/
setup: function (data, namespaces, eventHandle) {
//你可以在这里处理this的touchstart和touchend事件
},
/**
+ 卸载事件处理器 - this指向DOM元素
+ @namespaces 事件类型命名空间
*/
teardown: function (namespaces) {

}
};

我们看到jTap的处理似乎更高效一点。

除了以上两种依赖于库的解决方案之外,你还可以使用独立的FastClick库。

此外,我自己常用的一种解决办法是:

$('.targetEle').on('touchstart click',function(event){
event.preventDefault();

//do something

// 或者 return false;
});

这里要说明的是,webkit内核浏览器的touchstart、touchmove、touchend事件是基于鼠标事件模型的。所以我们可以用event.preventDefault();来阻止touchstart事件的默认行为。在支持touch事件的设备上,click事件就不会触发了;在不支持touch事件的设备上click事件可以正常使用。也可以使用return false;,它相当于下面两句:

event.stopPropagation();
event.preventDefault();

用这种办法可以避免在支持touch事件的设备上监听函数被调用两次(一次是touchstart事件,一次是click事件)。注意,这种处理方式在体验上并不好,因为用户的本意可能并不是要tap,可能是要滑动,然而手指刚一落下,交互就被触发了。用户的手指还没有离开屏幕交互就已经发生了,不符合用户经验。

1.3、 关于Windows Phone

Windows Phone的触摸事件是独立的事件模型,它与mouse事件无关。关于Windows phone上的pointer事件,我之前翻译的一篇让那些为Webkit优化的网站也能适配IE10 中的“第四步”有描述。

1.4、 多点触摸

关于多点触摸,我推荐hammer.js库。我们可以用它来实现旋转、放大、缩小等操作,你可以在这里查看它的Demo

1.5、 虚拟键盘

首先虚拟键盘会在input元素获取焦点的时候出现。input的type类型会对键盘界面有影响。tel、number类型会弹出数字键盘;email、url会弹出相应带有“@”符号和“.com”的键盘。当然这些不是绝对的,只是指定正确的type类型有机会创造一种较好的用户体验。
此外,虚拟键盘的出现会影响到fixed布局。一般是建议尽量使用absolute布局来解决。在iOS上有个经典的fixed布局bug,就是元素显示位置已发生变化,但是元素交互响应位置没有变化。需要触发一下自然滚动。你可以在这里查看如何修复这类bug。
另外,Android上虚拟键盘弹出会触发window的resize事件,而iOS上则不会。

二、导航条的变化

导航条应该是响应式布局中比较重要的点了。太过复杂的导航,我们可能在移动端直接替换掉,换成较为简单的符合移动设备屏幕的布局。比如澳大利亚旅游局中国官网,它的PC端布局中有一个较为复杂的二级导航,所以我们在移动端布局里部分替换了它的结构。

PC端的导航一般是位于页面顶端,然后过渡到移动端的时候它可能还是位于顶端的,此时出现一个常见的“三道杠”的菜单按钮。按钮点击导航下拉,如果不幸导航条是fixed,那么你就要考虑最好不要让导航内容太长而无法在当前屏幕高度下全部显示。幸好australia.cn的移动端导航不是太长。
australia.cn
还有一种很巧妙的方案是点击菜单按钮用锚点定位到页面底部,移动端的导航区域被安排在了这里。页面是可以滚动的,并且用户在浏览完当前页面后很自然地遇到导航,有引导用户到其他页面的机会。Adobe & HTML网站就用了这种方式。
adobe & html
此外,本博客的导航位于页面左侧。因为导航内容足够简单,所以在移动端的时候并没有做替换。而是把导航隐藏,用一个向右的箭头按钮提示用户点击后覆盖式拉出。
codingserf
在另外一个商场网站中,我们让移动端导航从右侧推出,同时整个页面向也会向左移动,以空出导航的空间。
金地广场
当然响应式的导航方式不止这几种,并且好的导航设计比比皆是,这里只是抛砖引玉。

三、动画和3D渲染

关于CSS3的动画,可以阅读我之前翻译的一篇CSS3的过渡和动画。我们这里想提及的是,很多时候我们想让一个元素的动画也是响应式的,或者元素本身是流动布局,我们想让位移动画也是百分比的。此时要注意,操作一个绝对定位元素的top、bottom、left、right属性要比操作它的margin效率更高,因这个元素已经脱离文档流了,不会造成回流。然而我们知道操作这些属性的百分比,他们的取值是基于父元素的,也就是说,我们让一个元素从left: 0;位移到left:100%,那这个100%就相当于父元素的宽度。当然这也是一种合理的应用场景。可是如果你想让一个元素只是移动自身的宽度的话,那我们就要用transform: translateX(100%)了。translate是相对于元素自身的。我们来看一个模拟页面滑动的例子,html代码如下:

<div class="view">
<div class="page_wrap">
<div class="page page_prev"><a href="#page_next">[点击我可以滑动到 下一页]</a></div>
<div class="page page_next"><a href="#page_prev">[点击我可以滑动到 上一页]</a></div>
</div>
</div>

css主要代码如下:

.view{
width: 320px;
overflow: hidden;
}
.page_wrap{
overflow: hidden;
width: 200%;
transition: all 0.5s;
}
.page{
float: left;
width: 50%;
}
.effect_turn{
transform: translateX(-50%);
}

我们打算通过给.page_wrap加减.effect_turn类让其实现左右翻页。我们看到.page_wrap的宽度是200%,里面每个.page的宽度是50%,其实这个50%正好是.view的宽度,相对于可视窗口也就是也就是320px。我们来看Demo代码。Demo里我并没有给.view指定具体宽度,因为它会继承窗口宽度,所以.page_wrap相当于两个窗口那么宽,而里面的每个宽度设为50%的.page相当于一个窗口的宽度。所以我们让.page_wrap移动自身的50%,就相当于移动了一个窗口宽度。就可以实现两个.page间的滑动切换。

另一点,记得要在使用CSS3动画效果时开启3D渲染,它能帮我们解决好多问题。一般我们会在执行动画的元素(或其父元素)上加上transfrom: translateZ(0);来开启3D渲染。其实这一过程是把动画区域视作2D图像提交到gpu去做渲染,此时如果我们做的是scale动画,在放大时可能导致元素模糊。所以一般会在动画结束后清除3D,还原2D。
iOS上的CSS位移动画由于效率的问题会产生残影,此时启动3D渲染效果会很好多。
我们常会看到这样一个效果,就是一个图片被一个div包裹着,鼠标滑过,图片从中心变大,但是显示区域还是外层div那么大。这个效果本没有问题,但是,如果我们给外层div加了个圆角,事情就不一样了。如图所示,在Chrome上,图片在动画过程中会溢出圆角,变成直角,动画结束圆角出现。(我们当然给外层div加了overflow: hidden;):
chrome bug
所以我们给外层div加了transform:translateZ(0);,问题就很圆满地解决了。详细代码可以看jsfiddle上的Demo
在移动端上操作translate时,即使强制3D渲染,似乎还是会导致溢出,因此我们在位移动画时直接让元素position:absolute;然后操作它的left,top,right,bottom。Demo在此

四、响应式插件

我之前有封装过一些响应式的插件,如响应式 3D Gallery 效果插件,也在文章中介绍了插件开发的思路。这个插件就使用了上面的translate的特性,利用它百分比是相对自身的特点让图片一个接一个的动起来。主要任务交给了流动布局和media queries,实现了插件的响应式。把主要的配置放在了CSS代码中。

五、总结

《响应式与移动端》这一系列的文章先写到这里。这一系列只是一个粗浅的入门指引,希望能起到抛砖引玉的作用。欢迎各位留言探讨。

 

 


响应式与移动端系列:
响应式与移动端(一):万事俱备了吗?
响应式与移动端(二):了不起的百分比
响应式与移动端(三):弹力十足的图文混排
响应式与移动端(四):会变脸的媒体查询
响应式与移动端(五):交互的过渡