在微信小程序里使用rpx,被坑了😕 | 不同设备表现不同
小小需求
实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。
放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。
开发
组件结构
按照上面说的过程,放好按钮,添加好点击事件。比较复杂的地方就是处理气泡的定位,气泡需要进行绝对定位,让它脱离文档流,不能在隐藏或偏移的时候还占个坑(需要实现那种浮动的效果)。还有气泡有个小三角,这个三角也是需要额外处理定位的。于是设计了组件的结构如下:
Tooltip 包着 Button 显示在界面上,设置定位属性,让它可以成为子元素定位的基准元素。然后创建 Prompt,它会相对于 Tooltip 进行定位,Prompt 中的小三角形则相对于 Prompt 进行定位。定位的具体数值则根据元素的尺寸和想放置的位置进行定位。在这个例子中就是实现气泡在按钮下方居中显示, Prompt 偏移数值计算如下:
水平偏移需要考虑 Tooltip 和 Prompt 的宽度,偏移的距离就是两者宽度之差的一半。
left=width(Tooltip)/2−width(Prompt)/2left = width(Tooltip) / 2 - width(Prompt) / 2left=width(Tooltip)/2−width(Prompt)/2
垂直偏移则要考虑 Tooltip、Prompt 和 小三角的高度,计算方法类似。(偷懒不做动图了)
top=height(Tooltip)+hegiht(::before_border)+height(gap)top = height(Tooltip) + hegiht(::before\_border) + height(gap)top=height(Tooltip)+hegiht(::before_border)+height(gap)
小三角相对于气泡的偏移也是类似的计算方法,总之能够根据元素的尺寸让偏移刚好能够居中。
代码
代码如下:(wxml 和 wxss 没有高亮,用 html 和 css 格式代替了)
<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;
position:absolute;
padding: 20rpx;
top: 80rpx;
left: 55rpx;
}
.prompt-container::before{
position:absolute;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
top: -28rpx;
left: 103rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}
踩坑
美滋滋呀,这就做完了,在 iPhone 12 mini 模拟器上看起来丝毫没有问题,整个气泡内容看起来是那么完美~
换个最豪(ang)华(gui)的机型(iPhone 14 Pro Max)看看,大事不妙,怎么小三角和气泡之间出现了一条缝,再看看代码按按计算器,算的尺寸没有任何问题呀!但是展示就是变成了这样:
爬坑
打开调试器一顿猛调试,发现了一些不对劲,下面慢慢说。
关于 rpx
微信小程序提供了个特殊的单位 rpx,代码中也都是使用这个单位进行开发,据说是能够方便开发者在不同尺寸设备上实现自适应布局。
放一张官方文档截图:
它的意思就是,它把所有的屏幕宽度都设置为 750rpx,不管这个设备真实的宽度有几个设备独立像素(就是宽度有多少 px)。开发者只需要使用 rpx 为单位,小程序会帮你把 rpx 转成 px,听起来是不是很方便很友好~(但并不是🌚)
试试这个公式是不是真的
根据图中提供的转换方式 1rpx=(screenWidth/750)×px1rpx = (screenWidth / 750) \times px1rpx=(screenWidth/750)×px,
用上面那个例子中的 Tooltip 组件来进行验证,手动算一下在设备上得到的 px 值是不是真的能用上面的公式计算出来。
设备 | 屏幕尺寸 / 750 | 组件 | 理论尺寸 | 真实尺寸 |
---|---|---|---|---|
iPhone 12 mini | 375 / 750 = 0.5 | Tooltip | 175 × 32 | 175 × 32 |
iPhone 14 Pro Max | 428 / 750 = 0.57 | Tooltip | 199.5 × 36.48 | 199× 36 |
看起来真实的计算会直接省略小数点后的值,直接进行取整。这可能是导致我们的预期和真实展示有偏差的原因。再看看其他组件的尺寸计算:
设备 | 屏幕尺寸 / 750 | 组件 | 理论尺寸 | 真实尺寸 |
---|---|---|---|---|
iPhone 12 mini | 375 / 750 = 0.5 | Prompt | 120 | 120 |
::before | 14 × 14 | 14 × 14 | ||
iPhone 14 Pro Max | 428 / 750 = 0.57 | Prompt | 136.8 | 136 |
::before | 15.96 × 15.96 | 14 × 14 |
因为高度是根据文字自适应的,所以这里没有计算 Prompt 的高度。但依然可以从表中看出,rpx 到 px 的转换并不是简单的直接取整,不然 iPhone 14 Pro Max 中的 ::before 应该尺寸为 15 × 15。至于到底是所有 rpx 到 px 转换都有隐藏的规则,还是伪元素的尺寸转换和其他元素不统一,还是 border 尺寸计算比较特殊,我们也无从得知,官方也没有相关说明。
小三角相对于气泡的偏移
其实在这个例子中,我们最关心的就是这个小三角相对于气泡的偏移是不是符合预期,整体气泡居中与否那么小的差别我们几乎看不出来,但是这个小三角偏离气泡这段距离,搁谁都无法接受。
那着重看下这个小三角的偏移我们是怎么做的,小三角的尺寸完全是由边构成的,完整的矩形尺寸是 28rpx×28rpx28rpx × 28rpx28rpx×28rpx,我们向上的偏移需要设置成整个矩形的尺寸,也就是 top:−28rpxtop: -28rpxtop:−28rpx,这样才能让下半部分的小三角完全展示出来。理论上来说,尺寸和偏移都设置 rpx 为单位,如果使用统一的转换规则,那肯定也是没问题的,既然出现了问题肯定是两者的计算不是那么的统一。我们看到实际的结果,尺寸计算是不符合我们的预期的,那么就猜测偏移可能是按照公式计算的。可以来验证一下,计算得到的值 151515 和真实值 141414 相差 1px1px1px,我打算放个高度为 1px1px1px 的长条在这个缝隙里,看看是不是刚好塞进去。
竟然真的刚好塞进去了,这说明我的猜测应该没有错,偏移的计算在我们预期中,小三角向上移动了 15px。但不能进行更多的验证了,再猜我就要把这个规律猜出来了(手动狗头🤪)。
总之就是,盒子尺寸的计算和偏移距离的计算用的不是一个规律,这就是坑之所在。
解决方案
针对当前需求
我们可以避开这个坑,让小三角相对于气泡不要产生偏移,而是能死死的贴在气泡上。想要达到这样的效果,我们需要修改一下布局结构,改成下面这个样子:
让气泡整体和小三角形成兄弟关系,那他俩就不会分离了,然后整体的偏移让他们的父节点 Prompt 来决定。
代码如下:
<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle"></view>
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>
.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
position:absolute;
top: 80rpx;
left: 55rpx;
}
.prompt-triangle{
position:relative;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
left: 103rpx;
}
.prompt-text-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;
padding: 20rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}
通用方法
除了上面避坑的方法,还有一个方法就是进行一个填坑。自定义实现一个 rpx2px 方法,动态的根据设备来进行 px 值的计算,再通过内联样式传递给元素。
function rpx2px(rpx){
return ( wx.getSystemInfoSync().windowWidth / 750) * rpx
}
Page({
data: {
top: rpx2px(100), // 类似这样定义一个状态,通过内联样式传入
},
})
<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle" style="top:{{top}}px"></view>
<!-- 注意上面👆这里添加了 style 内联样式 -->
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>
这样子不管是什么尺寸什么偏移都按照统一的规则进行换算,妈妈再也不同担心我被坑啦~~~
总结
虽然解法看起来如此简单,但是爬坑的过程真是无比艰难,各种猜测和假设,虽然一些得到了验证,但最终也是无法猜透小程序的心~~ 只能自己避坑和填坑,按需选择吧。
来源:juejin.cn/post/7257516901843763257