3、布局系统差异:CSS Flexbox vs 原生布局

做界面移植,布局系统是第一道坎。说白了,你在 Web 上用 Flexbox 写得飞起,到了 Android 和 iOS 那边,会发现「这玩意儿怎么对不上?」。我刚开始做跨平台移植时,就因为这个栽过跟头——一个简单的居中布局,Web 端一行代码搞定,原生端折腾了半天。

这一章,咱们就聊聊 Flexbox、Android LinearLayout 和 iOS Auto Layout 这三者的核心差异,以及单位换算的那些坑。

3.1 三大布局系统的核心哲学

先看一张图,理清它们各自的定位:

三大布局系统核心对比 CSS Flexbox Android LinearLayout iOS Auto Layout 核心特性对比 flex-direction 控制 orientation 属性 约束链控制 justify-content / align-items gravity / layout_gravity NSLayoutConstraint 对齐 flex: 1 等比例分配 layout_weight 权重 等宽约束 + multiplier 注意:Flexbox 是「弹性」布局,LinearLayout 是「线性」布局,Auto Layout 是「约束」布局

3.2 Flexbox vs LinearLayout:看似相似,实则不同

很多前端同学刚接触 Android 布局时,会觉得 LinearLayout 跟 Flexbox 差不多。嗯,表面上看确实像——都有主轴方向,都能控制对齐。但实际用起来,差别不小。

3.2.1 主轴方向控制

Flexbox 用 flex-direction 控制方向,LinearLayout 用 orientation。这个倒是对应的:

/* CSS Flexbox */
.container {
  display: flex;
  flex-direction: row;      /* 水平排列 */
  /* flex-direction: column; */  /* 垂直排列 */
}
<!-- Android LinearLayout -->
<LinearLayout
    android:orientation="horizontal"  <!-- 水平排列 -->
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <!-- 子视图 -->
</LinearLayout>

但有个关键区别:Flexbox 默认是 row,LinearLayout 默认是 horizontal。这个倒还好,真正坑人的在后面。

3.2.2 对齐方式:gravity vs justify-content

这里我开始踩坑了。Flexbox 用 justify-content 控制主轴对齐,align-items 控制交叉轴对齐。LinearLayout 呢?它用 gravity 和 layout_gravity。

我举个例子你就明白了:

/* Flexbox 水平居中 */
.container {
  display: flex;
  justify-content: center;  /* 主轴居中 */
  align-items: center;      /* 交叉轴居中 */
}
<!-- LinearLayout 水平居中 -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">
    <!-- 注意:gravity 同时控制主轴和交叉轴 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello" />
</LinearLayout>
我曾经踩过的坑:LinearLayout 的 gravity 是同时作用于主轴和交叉轴的。如果你只想水平居中但不要垂直居中,得用 gravity="center_horizontal"。而 Flexbox 的 justify-content 和 align-items 是分开的。这个差异,我第一次移植时没注意,结果所有按钮都跑到了中间位置。

3.2.3 权重分配:flex vs layout_weight

这个倒是比较直观的对应关系:

功能 CSS Flexbox Android LinearLayout
等比例分配 flex: 1 layout_weight="1"
固定宽度 + 剩余分配 flex: 0 0 100px layout_width="100dp" + layout_weight="0"
比例控制 flex: 2 vs flex: 1 layout_weight="2" vs layout_weight="1"

但有个细节:Android 的 layout_weight 需要配合 layout_width="0dp"(水平方向)或 layout_height="0dp"(垂直方向)使用。这个我一开始不知道,结果权重分配死活不对。

我的习惯:在 LinearLayout 中使用权重时,先把对应方向的尺寸设为 0dp,这样权重才能正确分配剩余空间。比如水平排列时写 android:layout_width="0dp"。

3.3 Flexbox vs Auto Layout:完全不同的思维方式

如果说 LinearLayout 跟 Flexbox 还算「亲戚」,那 Auto Layout 简直就是「外星人」。我第一次接触 Auto Layout 时,内心是崩溃的——什么?没有主轴?没有交叉轴?全靠约束?

3.3.1 约束链 vs 弹性盒子

Flexbox 的核心是「弹性盒子」——子元素在容器内自动排列。Auto Layout 的核心是「约束链」——每个视图的位置和大小由一组约束方程决定。

// iOS Auto Layout 示例
NSLayoutConstraint.activate([
    // 左边距 20
    view.leadingAnchor.constraint(equalTo: parent.leadingAnchor, constant: 20),
    // 顶部对齐
    view.topAnchor.constraint(equalTo: parent.topAnchor, constant: 10),
    // 宽度 100
    view.widthAnchor.constraint(equalToConstant: 100),
    // 高度 50
    view.heightAnchor.constraint(equalToConstant: 50)
])

你看,每个约束都是独立的。没有「容器自动排列子元素」这个概念,你得手动告诉每个视图它应该在哪。

3.3.2 等宽布局的差异

在 Flexbox 里,等宽布局很简单:

.container {
  display: flex;
}
.item {
  flex: 1;
}

在 Auto Layout 里,你得这样写:

// 三个等宽按钮
NSLayoutConstraint.activate([
    button1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    button1.trailingAnchor.constraint(equalTo: button2.leadingAnchor),
    button2.trailingAnchor.constraint(equalTo: button3.leadingAnchor),
    button3.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    button1.widthAnchor.constraint(equalTo: button2.widthAnchor),
    button2.widthAnchor.constraint(equalTo: button3.widthAnchor)
])

嗯,你没看错。三个等宽按钮,Flexbox 一行代码,Auto Layout 六行约束。这就是为什么很多 iOS 开发者喜欢用第三方库(比如 SnapKit)来简化约束写法。

核心差异总结:
  • Flexbox:容器驱动,子元素自动排列
  • LinearLayout:容器驱动,但只有线性排列
  • Auto Layout:约束驱动,每个视图独立定义位置

3.4 单位换算:百分比、dp、sp 的坑

布局系统搞定了,单位换算又是一道坎。Web 上用百分比和 px 用惯了,到了移动端发现还有 dp、sp、pt 这些单位。

3.4.1 百分比 vs dp

Flexbox 里,你可以用百分比来定义宽度:

.item {
  width: 50%;  /* 父容器宽度的一半 */
}

但在 Android 里,LinearLayout 不支持百分比。你得用权重或者写死 dp 值。不过 Android 有个 PercentRelativeLayout 和 PercentFrameLayout(来自 support 库),但现在已经不推荐用了。

我的建议是:用 layout_weight 替代百分比。比如 50% 宽度,可以写成:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <View
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1" />
    <View
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1" />
</LinearLayout>

3.4.2 dp、sp、pt 的区别

这个表格帮你快速理解:

单位 全称 用途 特点
dp Density-independent Pixels 布局尺寸、边距 在不同密度屏幕上保持物理尺寸一致
sp Scale-independent Pixels 字体大小 跟随用户系统字体缩放设置
pt Points iOS 布局和字体 1pt = 1/72 英寸
px Pixels 不推荐直接使用 不同屏幕密度下表现不一致
我曾经踩过的坑:在 Android 上用 sp 设置布局宽度。结果用户把系统字体调到最大,布局直接崩了。记住:sp 只用于字体,布局尺寸永远用 dp。

3.4.3 单位换算公式

如果你需要在代码里手动换算,记住这个公式:

// dp 转 px
px = dp * (density / 160)

// 其中 density 是屏幕密度
// mdpi: 160  -> 1dp = 1px
// hdpi: 240  -> 1dp = 1.5px
// xhdpi: 320 -> 1dp = 2px
// xxhdpi: 480 -> 1dp = 3px

在 iOS 上,pt 和 px 的关系更简单:

// 非 Retina 屏:1pt = 1px
// Retina 屏:1pt = 2px
// Super Retina 屏:1pt = 3px

3.5 移植实战:一个居中布局的三种写法

最后,咱们看一个实际例子。一个简单的居中布局:一个按钮在屏幕正中间。

Flexbox 版本:

<div style="display: flex; justify-content: center; align-items: center; height: 100vh;">
  <button>点击我</button>
</div>

LinearLayout 版本:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击我" />
</LinearLayout>

Auto Layout 版本:

NSLayoutConstraint.activate([
    button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])

你看,最简单的居中布局,三种写法差异就这么大。更复杂的布局,差异只会更明显。

我的移植习惯:先画出布局的「约束图」,明确每个视图的位置关系。然后再分别用目标平台的布局系统实现。这样能避免「用 Flexbox 的思维写 LinearLayout」的尴尬。

嗯,布局系统的差异就聊到这儿。记住一句话:不要试图用一种布局系统的思维去写另一种。每个平台都有自己的设计哲学,尊重它,才能写出好的界面。