1、问题现象
在一个用 uni-app 开发的微信小程序页面里,有一个空状态页:当没有数据时,页面显示一张铺满屏幕的背景图,左上角叠了一行提示文字(比如「暂无数据」),下方还有一个通知条。
用户在 iPhone 上长按屏幕并向下拖拽时,发现这行提示文字和下方的通知条像被「拽」了下来------手指拖到哪它们跟到哪,松手后又自动滑回原位。而背景图、底部的卡片都纹丝不动。
期望:长按下拉时,文字不应该被「拉」下来。
2、背景知识:iOS 橡皮筋滚动(Rubber Band Scrolling)
iOS 的 WebView 有一个原生特性叫橡皮筋效果(rubber band / 弹性滚动):当页面滚动到边界(顶部或底部)后继续拖拽,内容会被「拉」出边界,松手后回弹。这是系统级行为,无法通过常规手段彻底关闭。
关键点在于:橡皮筋拖动的是「文档流」里的内容。哪些元素会跟着动、哪些不会,取决于它们的定位方式。
3、最初分析
最初的判断是这样的:
- 提示文字用了
position: absolute,定位在父容器内的某个坐标 - 父容器是普通文档流里的一个 div
- 橡皮筋拖动时,父容器跟着动,里面 absolute 的文字也跟着动
于是提出「方案 B」:把文字从 absolute 改成普通文档流元素(用 flex 布局 + margin/padding 定位),让它和背景图「一起动」,这样就不会有「单独被拽」的违和感。
scss
/* 改动前 */
.empty-tip {
position: absolute;
top: 7%;
left: 4%;
}
/* 方案 B(错误的修复) */
.empty-page {
justify-content: flex-start;
padding-top: 7vh; /* 把文字顶下去 */
}
.empty-tip {
align-self: flex-start;
margin-left: 4%;
}
结果:没有效果,文字照样能被拉下来。
而且更糟的是:.empty-page 原本是 height: 100vh,我又加了 padding-top: 7vh,导致它的总高度变成 107vh,超出了视口------页面凭空多出了可滚动高度,问题反而被放大了。
4、为什么方案 B 失败了?
方案 B 的前提是「让文字和背景图一起动」。但这个前提根本不成立。
排查时去看了背景图的样式,发现:
scss
.bg-image {
position: fixed; /* ← 关键 */
width: 100%;
top: 0;
bottom: 0;
}
背景图是 position: fixed。底部的卡片也是 position: fixed。
position: fixed 的元素相对视口定位 ,被浏览器放在独立的合成层里,橡皮筋滚动移动的是文档流,fixed 元素根本不跟随。
所以真相是:
- 背景图:
fixed→ 永远不动 - 文字:普通文档流 /
absolute(相对文档流父容器)→ 永远跟着橡皮筋弹
「让 fixed 和文档流元素一起动」是不可能的------一个永远不动,一个永远动。方案 B 的方向从一开始就错了。
5、真正的根因
把定位方式和橡皮筋的关系理清楚:
| 定位方式 | 参照物 | 橡皮筋拖动时 |
|---|---|---|
默认 / relative |
文档流 | 跟着动 |
absolute |
最近的定位祖先 | 如果祖先在文档流里,跟着动 |
fixed |
视口 | 不动 |
这个页面的提示文字是 position: absolute,它的定位祖先是一个 position: relative 的普通 div,这个 div 在文档流里,会被橡皮筋拖动,所以 absolute 的文字也跟着被拖。
这个页面其实已经有一套约定了 :凡是不该跟随滚动的元素(背景图、底部卡片)都用 position: fixed。提示文字和通知条是这套约定里的「漏网之鱼」------它们本该 fixed,却用了 absolute。
6、解决方案
把提示文字和通知条也改成 position: fixed,和页面已有的模式保持一致:
scss
/* 改动前 */
.empty-tip {
position: absolute;
top: 7%;
left: 4%;
}
.notice-bar {
position: absolute;
left: 4%;
top: calc(8% + 60rpx);
}
/* 改动后 */
.empty-tip {
position: fixed;
top: 7%;
left: 4%;
}
.notice-bar {
position: fixed;
left: 4%;
top: calc(8% + 60rpx);
}
为什么视觉位置不用调整?
原来 absolute 的 top: 7% 是相对父容器(height: 100vh)算的,即 7vh。改成 fixed 后 top: 7% 是相对视口算的,也是 7vh。因为父容器本来就是 100vh 且从视口顶部起算,两个百分比的基准完全一致,所以位置不变,只改 position 这一个属性即可。
改完后,文字和通知条锚定视口,和背景图行为一致,长按下拉时不再被「拽」下来。
7、复盘与排查思路
-
现象是「相对运动」,就去找参照物的差异。 用户感知到的是「文字相对背景动了」。一个动一个不动,说明它俩定位方式不同。直接对比两者的
position就能定位问题。 -
不要急于下结论,先看相关元素的实际样式。 第一次分析时假设了「文字和背景都在文档流里」,没有去核实背景图的样式,结果方案建立在错误前提上。如果一开始就 grep 一下背景图的
position,能少走很多弯路。 -
改样式时警惕「尺寸叠加」。 给一个已经是
height: 100vh的容器加padding-top,会让它变成107vh(默认box-sizing: content-box)。容器溢出视口会凭空制造可滚动区域,反而加剧橡皮筋问题。 -
尊重代码里已有的约定。 这个页面已经用
position: fixed标记了「不随滚动的元素」。最干净的修复不是发明新方案,而是让漏网的元素回归到已有的约定里。 -
定位方式速查: 怕橡皮筋 / 怕跟随滚动 → 用
fixed;absolute只在「定位祖先本身不动」时才安全。