花式提升移动端交互体验

本文总结了一些和移动端交互体验相关的小技巧,其实并没有很「花」

主要的知识点如下:

  • 禁止微信页面及iOS Safari拖动露底效果 (橡皮筋)
  • 禁止不必要的选择区域
  • 合理规划用户操作区域
  • 扩大点击区
  • 负Margin实现固定栏
  • 频繁操作使用节流函数



禁止微信页面及iOS Safari拖动露底效果 (橡皮筋)

上图就是露底效果 (橡皮筋效果)

其实不止iOS,安卓微信也会有这种露底的效果,为了更接近原生app的体验,建议禁止最外层的橡皮筋效果,防止webview或页面露出底部容器。

仔细使用可以发现,当滑动到顶部,继续往下滑时,容器会暴露;滑动到底部,继续往上滑,容器会暴露。
本质是一个 touchmove 事件不断向上冒泡,尝试拉开自身的容器,如果一直无法拉开冒泡到 webview 时,就出现“露底”。
解决思路很明显:当容器可以滑动时,若已经在顶部,禁止下滑;若在底部,禁止上滑;容器无法滚动时,禁止上下滑。

基本实现方式是通过 document 上监听 touchstarttouchmove 事件,判断滑动方向;判断滑动事件的触发 target 祖先元素是否有可滑动元素,无则直接阻止冒泡。
若有这种祖先元素,判断其状态:offsetHeight >= scrollHeight 高度不够发生滚动,阻止冒泡;若可滚动 scrollTop === 0 即在顶部,阻止下滑的冒泡;若 scrollTop + offsetHeight >= scrollHeight 已经滑到底,阻止上滑的冒泡。

还需注意的是,要先判断滑动时 x 轴位移是否大于 y 轴 ,即是否是水平滑动,防止“误伤”一些水平滚动的元素。
下面是一个示例代码(class="scroller"的元素被标记为可滚动元素)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const _ = require('src/util')
export default function (option) {
var scrollSelector = option.scroll || '.scroller'
var pos = {
x: 0,
y: 0
}

function stopEvent (e) {
e.preventDefault()
e.stopPropagation()
}

function recordPosition (e) {
pos.x = e.touches[0].clientX
pos.y = e.touches[0].clientY
}

function watchTouchMove (e) {
var target = e.target
var parents = _.parents(target, scrollSelector)
var el = null
if (target.classList.contains(scrollSelector)) el = target
else if (parents.length) el = parents[0]
else return stopEvent(e)
var dx = e.touches[0].clientX - pos.x
var dy = e.touches[0].clientY - pos.y
var direction = dy > 0 ? '10' : '01'
var scrollTop = el.scrollTop
var scrollHeight = el.scrollHeight
var offsetHeight = el.offsetHeight
var isVertical = Math.abs(dx) < Math.abs(dy)
var status = '11'
if (scrollTop === 0) {
status = offsetHeight >= scrollHeight ? '00' : '01'
} else if (scrollTop + offsetHeight >= scrollHeight) {
status = '10'
}
if (status !== '11' && isVertical && !(parseInt(status, 2) & parseInt(direction, 2))) return stopEvent(e)
}
document.addEventListener('touchstart', recordPosition, false)
document.addEventListener('touchmove', watchTouchMove, false)
}



禁止不必要的选择区域

如上左图,用户在手指停留过久的时候无意间触发了选择动作,但实际上这块区域没有被选择的必要,这影响了用户正常的操作。
这些标签可以使用 .no-select { user-select: none; } 解决(前缀交给处理器吧)
这个子元素会继承这个属性,所以只需要在稍微顶层的元素写就行,对于一些长文本内容,不应该添加这个属性,用户可能会有选择的需求。

开发中可以用cmd + a全选页面内容,如上右图,蓝色的即为当前可选区域(截图中的区域应该都禁止选择)


合理规划用户操作区域

用户最方便且单手能操作的区域如上左图蓝色区所示,应该确保用户的大部分点、触、滑、输入操作都集中在这块;更为常用的、高频的操作应该集中在这块。

如上图的输入框,远离了用户操作的输入区,应该只成为一个入口。所以在这里的交互设计上,选择点击后弹出一个真正的输入区,如右图。(模仿Airbnb)

如此引导用户进入舒适区做进一步交互。

不过左上的关闭按钮也是一个槽点、、、


扩大点击区

这个其实在上一篇关于移动端WEB开发的笔记里已经提到过,这里重新说一下。

移动端页面的点触事件是用户通过手指直接触发的,这和PC页面上鼠标触发的按钮不同,需要给予用户一定的点击容错率。

我们扩大按钮的实际触发区域,一般情况下可以使用padding去做,缺点在于可能会影响布局;

另一种方案是使用垫在元素底部的伪类::after / ::before去扩大,不会影响布局,缺点是会增加复合层数量。

具体使用哪种看取舍。


负Margin实现固定栏

负margin可以代替fixed,用正常流布局实现顶部栏 / 底部栏固定。


频繁操作使用节流函数

大部分同学应该都知道节流是个啥,不赘述;要提的一点是,节流函数可以用requestAnimationFrame 或者 requestIdleCallback 去实现。
一个节流函数的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function _throttle (fn, time) {
let running = false
let isFrame = false
if (!time) isFrame = true
const getCallback = (self, args) => {
return () => {
fn.apply(self, args)
running = false
}
}
return function () {
if (running) return
running = true
if (isFrame) {
window.requestAnimationFrame(getCallback(this, arguments))
} else {
setTimeout(getCallback(this, arguments), time)
}
}
}