4、二维缩放变换:关于原点的缩放,关于任意点的缩放
缩放变换,说白了就是把图形拉大或缩小。你想想看,在UI适配里,一个按钮从120x40变成60x20,这就是缩放。但这里有个坑——缩放中心不同,结果完全不同。
我个人习惯把缩放分成两类来理解:关于原点的缩放和关于任意点的缩放。前者是基础,后者是实战中真正用到的。
4.1 关于原点的缩放
这是最简单的缩放形式。假设有一个点P(x, y),我们想把它相对于原点(0,0)缩放s倍。新坐标就是:
x' = x * s
y' = y * s
写成矩阵形式更清爽:
[x'] [s 0] [x]
[y'] = [0 s] [y]
嗯,这里要注意:s是缩放因子。s > 1是放大,0 < s < 1是缩小。如果s是负数,那就是镜像翻转了——我在项目中遇到过,有次不小心传了个负值,整个UI图标都翻了个个儿,排查了半天才发现。
核心要点:关于原点的缩放,就是每个坐标分量乘以缩放因子。简单,但局限性很大——现实中很少以原点为缩放中心。
4.2 关于任意点的缩放
这才是实战中的常态。比如你要缩放一个按钮,通常是以按钮中心为基准,而不是以屏幕左上角。为什么?因为以中心缩放,按钮位置变化最小,用户体验最好。
关于任意点Pc(xc, yc)的缩放,步骤其实就三步:
- 平移:把Pc移到原点
- 缩放:关于原点缩放
- 平移回去:把原点移回Pc
写成矩阵连乘就是:
[x'] [1 0 xc] [s 0 0] [1 0 -xc] [x]
[y'] = [0 1 yc] [0 s 0] [0 1 -yc] [y]
[1 ] [0 0 1 ] [0 0 1] [0 0 1 ] [1]
化简后得到:
x' = s * x + (1 - s) * xc
y' = s * y + (1 - s) * yc
我的经验:在实际编码中,我从来不直接套用这个公式。而是用矩阵连乘的方式写代码,这样后续如果要叠加旋转、平移,只需要在矩阵链里加一项就行,维护起来特别方便。
4.3 缩放变换的矩阵表示
用齐次坐标,缩放矩阵可以统一表示为:
// 关于原点的缩放矩阵
S(sx, sy) = | sx 0 0 |
| 0 sy 0 |
| 0 0 1 |
// 关于任意点Pc的缩放矩阵
S(Pc, s) = T(Pc) * S(s, s) * T(-Pc)
这里sx和sy可以不同,实现非均匀缩放。我在做嵌入式UI适配时,经常遇到屏幕分辨率不是正方形的情况,这时候就需要用非均匀缩放来适配。
我曾经踩过的坑:非均匀缩放时,如果sx和sy相差太大,图形会严重变形。比如一个圆形图标,sx=2, sy=1,就变成了椭圆。在UI设计中,除非你刻意追求这种效果,否则尽量保持sx = sy。
4.4 实战:UI适配中的缩放应用
举个具体的例子。假设你有一个设计稿,分辨率是1920x1080。现在要适配到1280x720的屏幕上。缩放因子怎么算?
sx = 1280 / 1920 ≈ 0.667
sy = 720 / 1080 ≈ 0.667
如果两个因子相等,那就是等比例缩放。如果不相等,就需要考虑是裁剪还是留黑边了。
我个人习惯的做法是:
- 等比例缩放:取min(sx, sy),保证内容完整显示
- 非等比例缩放:分别用sx和sy,但要注意UI元素不要变形
你想想看,一个按钮在1920屏幕上居中的,缩放后还能居中吗?这就要用到关于任意点的缩放了——以屏幕中心为缩放点,位置关系才能保持。
4.5 知识体系图
下面这张图展示了缩放变换的核心逻辑:
4.6 代码实现示例
下面是我在嵌入式项目中常用的缩放函数,用C语言写的:
// 关于任意点缩放
void scale_around_point(float* x, float* y, float cx, float cy, float s) {
// 平移至原点
float tx = *x - cx;
float ty = *y - cy;
// 缩放
tx *= s;
ty *= s;
// 平移回去
*x = tx + cx;
*y = ty + cy;
}
// 矩阵方式实现(推荐)
void scale_matrix(float* x, float* y, float cx, float cy, float s) {
// 直接使用化简后的公式
*x = s * (*x) + (1 - s) * cx;
*y = s * (*y) + (1 - s) * cy;
}
我的建议:在性能敏感的嵌入式环境中,用化简后的公式直接计算,比矩阵连乘快得多。但如果你的代码需要频繁叠加多种变换,还是用矩阵方式更清晰,维护成本更低。
好了,关于二维缩放变换就聊这么多。记住一句话:缩放中心决定一切。搞清楚了原点缩放和任意点缩放的关系,你在做UI适配时就能游刃有余了。