Skip to content

09_响应式布局

目录

  1. 响应式布局概述
  2. Android 多屏幕适配挑战
  3. 资源限定符详解
  4. ConstraintLayout 响应式设计
  5. 多屏幕适配方案
  6. 折叠屏适配
  7. 横竖屏适配
  8. Material 3 自适应组件
  9. Jetpack Compose 响应式
  10. 性能优化
  11. 面试考点汇总

1. 响应式布局概述

1.1 什么是响应式布局?

**响应式布局(Responsive Layout)**是指能够根据设备的屏幕尺寸、分辨率、方向、密度等特性,自动调整 UI 布局和内容的布局设计方式。

核心目标:

  • ✅ 在不同设备上提供一致的用户体验
  • ✅ 最大化利用屏幕空间
  • ✅ 适应各种屏幕尺寸和比例
  • ✅ 支持横竖屏切换
  • ✅ 适配折叠屏等新形态设备

1.2 Android 屏幕碎片化问题

┌─────────────────────────────────────────────────────────┐
│                    Android 设备矩阵                      │
├──────────────┬──────────────┬──────────────┬───────────┤
│   手机       │   平板       │   折叠屏     │   电视    │
│  (4-7 英寸)   │  (7-13 英寸)  │  (7-13 英寸)  │ (>10 英寸) │
├──────────────┼──────────────┼──────────────┼───────────┤
│ 320x480      │ 768x1024     │ 2000+px      │ 1920x1080 │
│ 480x800      │ 800x1280     │ 可变尺寸     │ 3840x2160 │
│ 1080x1920    │ 1600x2560    │ 多窗口       │ 4K/8K     │
│ 1440x3000    │ 1920x2880    │ 分屏         │           │
└──────────────┴──────────────┴──────────────┴───────────┘

碎片化挑战:

  1. 尺寸多样:从 3.5 寸到 13 寸,超过 10 种主流尺寸
  2. 密度各异:mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi
  3. 比例复杂:16:9、18:9、19.5:9、20:9
  4. 形态创新:折叠屏、卷轴屏、分屏

1.3 响应式布局的演变

传统布局 (2008-2014)
├─ 固定尺寸 (dp)
├─ 简单百分比
└─ 资源限定符
        
    ↓ (碎片化加剧)
    
相对布局 (2014-2017)
├─ RelativeLayout
├─ 嵌套布局
└─ 手动适配
        
    ↓ (复杂度爆炸)
    
约束布局 (2017-2020)
├─ ConstraintLayout
├─ 链和偏置
├─ Guideline 和 Barrier
└─ 响应式约束
        
    ↓ (折叠屏时代)
    
现代响应式 (2020-至今)
├─ Material 3 Adaptive
├─ Jetpack Compose
├─ Foldables API
└─ Window Metrics API

2. Android 多屏幕适配挑战

2.1 度量单位详解

2.1.1 px(像素)

kotlin
// ❌ 不推荐使用 px
android:layout_width="100px"  // 在不同密度设备上显示大小不一致

问题: 100px 在 mdpi 和 xxhdpi 设备上实际显示大小相差 3 倍。

2.1.2 dp/dip(密度无关像素)

kotlin
// ✅ 推荐使用 dp
android:layout_width="100dp"  // 在不同密度设备上视觉大小一致

计算公式:

dp = px / (density / 160)
密度DPI比例1dp = ?px
mdpi1601.0x1px
hdpi2401.5x1.5px
xhdpi3202.0x2px
xxhdpi4803.0x3px
xxxhdpi6404.0x4px

2.1.3 sp(缩放无关像素)

kotlin
// ✅ 字体推荐使用 sp(支持用户字体大小设置)
android:textSize="16sp"

与 dp 的区别: sp 会根据用户的字体大小偏好进行缩放。

2.1.4 百分比和权重

xml
<!-- 百分比布局 -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <View
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />  <!-- 占 50% -->
    
    <View
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />  <!-- 占 50% -->
</LinearLayout>

2.1.5 ConstraintLayout 中的百分比

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <View
        android:id="@+id/view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="10%"
        android:layout_marginEnd="10%"
        app:layout_constraintWidth_percent="0.8"
        app:layout_constraintHeight_percent="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

2.2 屏幕配置参数

kotlin
// 获取屏幕信息
val displayMetrics = resources.displayMetrics

// 宽度(px)
val widthPx = displayMetrics.widthPixels

// 高度(px)
val heightPx = displayMetrics.heightPixels

// 密度
val density = displayMetrics.density

// DPI
val dpi = displayMetrics.densityDpi

// 转换为 dp
val widthDp = (widthPx / density).toInt()
val heightDp = (heightPx / density).toInt()

2.3 屏幕分类标准

小屏幕 (Small):  < 4.7 英寸
普通屏幕 (Normal): 4.7 - 7 英寸
大屏幕 (Large):    7 - 10 英寸
超大屏幕 (XLarge):  > 10 英寸
xml
<!-- 在 themes 中定义不同屏幕的配置 -->
<!-- values/themes.xml (默认) -->
<dimen name="activity_margin">16dp</dimen>

<!-- values-sw600dp/themes.xml (768dp+ 平板) -->
<dimen name="activity_margin">32dp</dimen>

<!-- values-sw720dp/themes.xml (大平板) -->
<dimen name="activity_margin">48dp</dimen>

3. 资源限定符详解

3.1 限定符优先级

最高
  ├─ 配置限定符 (配置最具体)
  ├─ 窄屏 + 宽屏
  ├─ 最小宽度 (sw<N>dp)
  ├─ 屏幕尺寸 (large, xlarge)
  ├─ 屏幕方向 (port, land)
  ├─ 屏幕长宽比 (long, notlong)
  ├─ UI 模式 (night, car)
  ├─ 语言区域 (zh, en, zh-rCN)
  ├─ 键盘类型 (keysexposed, keyshidden)
  ├─ 导航类型 (navexposed, nonav)
  ├─ 屏幕密度 (mdpi, xhdpi)
  ├─ 屏幕高度 (h<N>dp)
  ├─ 屏幕宽度 (w<N>dp)
  └─ 最低

3.2 最小宽度限定符 (sw<N>dp)

最推荐的适配方式!

┌─────────────────────────────────────────────────┐
│           最小宽度限定符 (smallest-width)         │
├─────────────────────────────────────────────────┤
│ sw320dp  - 几乎所有手机                          │
│ sw360dp  - 主流手机                              │
│ sw480dp  - 大屏手机 / 小平板横屏                  │
│ sw600dp  - 7 英寸平板                             │
│ sw720dp  - 10 英寸平板                            │
│ sw840dp  - 大平板                                │
│ sw1280dp - 超大屏 / 电视                         │
└─────────────────────────────────────────────────┘

使用示例:

项目结构:
├── res/
│   ├── layout/              # 默认布局(手机竖屏)
│   │   └── activity_main.xml
│   ├── layout-sw600dp/      # 最小宽度 600dp(7 英寸平板)
│   │   └── activity_main.xml
│   ├── layout-sw720dp/      # 最小宽度 720dp(10 英寸平板)
│   │   └── activity_main.xml
│   └── layout-sw840dp/      # 最小宽度 840dp(大平板)
│       └── activity_main.xml
xml
<!-- res/layout/activity_main.xml (手机,单列) -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

<!-- res/layout-sw600dp/activity_main.xml (平板,双栏) -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    
    <FrameLayout
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    
    <FrameLayout
        android:id="@+id/detail"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="2" />
</LinearLayout>

3.3 屏幕方向限定符

├── res/
│   ├── layout/              # 默认布局
│   │   └── activity_main.xml
│   ├── layout-port/         # 竖屏专用
│   │   └── activity_main.xml
│   └── layout-land/         # 横屏专用
│       └── activity_main.xml
xml
<!-- res/layout/activity_main.xml (默认,竖屏) -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!-- 竖屏布局 -->
</LinearLayout>

<!-- res/layout-land/activity_main.xml (横屏) -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <!-- 横屏布局 -->
</LinearLayout>

3.4 夜间模式限定符

├── res/
│   ├── values/              # 日间主题
│   │   └── themes.xml
│   └── values-night/        # 夜间主题
│       └── themes.xml
xml
<!-- res/values/themes.xml (日间) -->
<resources>
    <color name="background">#FFFFFF</color>
    <color name="text_primary">#000000</color>
    <color name="text_secondary">#666666</color>
</resources>

<!-- res/values-night/themes.xml (夜间) -->
<resources>
    <color name="background">#121212</color>
    <color name="text_primary">#FFFFFF</color>
    <color name="text_secondary">#B0B0B0</color>
</resources>

3.5 语言区域限定符

├── res/
│   ├── values/              # 默认语言
│   │   └── strings.xml
│   ├── values-zh/           # 简体中文
│   │   └── strings.xml
│   ├── values-zh-rCN/       # 中国大陆
│   │   └── strings.xml
│   ├── values-zh-rTW/       # 繁体中文(台湾)
│   │   └── strings.xml
│   └── values-en-rUS/       # 美式英语
│       └── strings.xml

3.6 联合限定符

├── res/
│   └── layout-sw600dp-land/  # 最小宽度 600dp 且横屏
│       └── activity_main.xml

常见组合:

  • values-night-zh-rCN/ - 夜间简体中文(中国大陆)
  • layout-sw600dp-port/ - 平板竖屏
  • drawable-xxhdpi-night/ - 夜间高 DPI 图片

3.7 配置查询

kotlin
// 获取当前配置
val configuration = resources.configuration

// 屏幕方向
val orientation = configuration.orientation
// ORIENTATION_PORTRAIT (竖屏)
// ORIENTATION_LANDSCAPE (横屏)

// UI 模式(夜间模式)
val uiMode = configuration.uiMode
val isNight = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == 
              Configuration.UI_MODE_NIGHT_YES

// 屏幕尺寸
val screenLayout = configuration.screenLayout
val screenSize = screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK
// SCREENLAYOUT_SIZE_SMALL
// SCREENLAYOUT_SIZE_NORMAL
// SCREENLAYOUT_SIZE_LARGE
// SCREENLAYOUT_SIZE_XLARGE

// 最小宽度 (dp)
val smallestScreenWidthDp = configuration.smallestScreenWidthDp

3.8 动态切换配置

kotlin
// 动态切换夜间模式
private fun toggleNightMode() {
    val isNight = (resources.configuration.uiMode and 
                   Configuration.UI_MODE_NIGHT_MASK) == 
                  Configuration.UI_MODE_NIGHT_YES
    
    val newNightMode = if (isNight) {
        AppCompatDelegate.MODE_NIGHT_NO
    } else {
        AppCompatDelegate.MODE_NIGHT_YES
    }
    
    AppCompatDelegate.setDefaultNightMode(newNightMode)
    
    // 重新创建 Activity 应用新配置
    recreate()
}

// 动态切换语言
private fun setLanguage(locale: Locale) {
    val context = applicationContext
    val resources = context.resources
    val configuration = resources.configuration
    
    configuration.setLocale(locale)
    configuration.setLayoutDirection(locale)
    
    context.createConfigurationContext(configuration)
    
    // 重启应用或 Activity
}

4. ConstraintLayout 响应式设计

4.1 ConstraintLayout 优势

传统布局 vs ConstraintLayout
┌─────────────┬──────────────────┬──────────────────┐
│   特性      │  嵌套布局         │ ConstraintLayout │
├─────────────┼──────────────────┼──────────────────┤
│ 视图层级    │ 深层嵌套 (5-10 层)  │ 扁平化 (1-2 层)    │
│ 性能        │ 较低              │ 高               │
│ 灵活性      │ 差                │ 优秀             │
│ 响应式支持  │ 弱                │ 强               │
│ 设计器支持  │ 一般              │ 优秀             │
└─────────────┴──────────────────┴──────────────────┘

4.2 基础约束

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 基础约束:上下左右 -->
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="标题"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
    
    <!-- 居中约束 -->
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮"
        app:layout_constraintTop_toBottomOf="@id/title"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4.3 Guideline(指导线)

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 垂直指导线(从左边 30% 位置) -->
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/vertical_guide"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintGuide_percent="0.3"
        app:layout_constraintGuide_begin="100dp" />
    
    <!-- 水平指导线(从顶部 20% 位置) -->
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/horizontal_guide"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintGuide_percent="0.2" />
    
    <!-- 使用指导线定位 View -->
    <ImageView
        android:id="@+id/avatar"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintWidth_percent="0.25"
        app:layout_constraintHeight_percent="0.25"
        app:layout_constraintStart_toStartOf="@id/vertical_guide"
        app:layout_constraintTop_toTopOf="@id/horizontal_guide" />
</androidx.constraintlayout.widget.ConstraintLayout>

4.4 Barrier(屏障)

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 三个按钮 -->
    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="短"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
    
    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="中等长度按钮"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toEndOf="@id/btn1" />
    
    <Button
        android:id="@+id/btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是一个很长的按钮"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toEndOf="@id/btn2" />
    
    <!-- 屏障:在所有按钮的右侧 -->
    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="end"
        app:constraint_referenced_ids="btn1,btn2,btn3" />
    
    <!-- 内容在屏障右侧 -->
    <TextView
        android:id="@+id/content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@id/barrier"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4.5 Chain(链)

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 链:水平排列三个按钮 -->
    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮 1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBaseline_toBaselineOf="@id/btn2" />
    
    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮 2"
        app:layout_constraintStart_toEndOf="@id/btn1"
        app:layout_constraintEnd_toStartOf="@id/btn3"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBaseline_toBaselineOf="@id/btn3" />
    
    <Button
        android:id="@+id/btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮 3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    
    <!-- 链样式:spaced(等间距)、packed(紧凑)、spread(等分布) -->
    <Button
        android:id="@+id/btn4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮 4"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn1"
        app:layout_chainStyle="spaced" />
    
    <Button
        android:id="@+id/btn5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮 5"
        app:layout_constraintStart_toEndOf="@id/btn4"
        app:layout_constraintEnd_toStartOf="@id/btn6" />
    
    <Button
        android:id="@+id/btn6"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮 6"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4.6 百分比布局

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 宽度占父容器 50%,高度占父容器 30% -->
    <ImageView
        android:id="@+id/image"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintWidth_percent="0.5"
        app:layout_constraintHeight_percent="0.3"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
    
    <!-- 边距百分比 -->
    <View
        android:id="@+id/content"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="10%"
        android:layout_marginEnd="10%"
        android:layout_marginTop="5%"
        android:layout_marginBottom="5%"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4.7 宽高比(Ratio)

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 16:9 视频容器 -->
    <FrameLayout
        android:id="@+id/video_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    
    <!-- 1:1 正方形 -->
    <ImageView
        android:id="@+id/avatar"
        android:layout_width="100dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintTop_toBottomOf="@id/video_container"
        app:layout_constraintStart_toStartOf="parent" />
    
    <!-- 可变宽高比 -->
    <androidx.cardview.widget.CardView
        android:id="@+id/card"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="W,1:1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4.8 响应式约束

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 响应式文本(根据可用空间调整字号) -->
    <TextView
        android:id="@+id/responsive_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="这是一个较长的文本,会根据可用空间自动调整字号"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:fontVariationSettings="wght 400"
        app:autoSizeTextType="uniform"
        app:autoSizeMaxTextSize="24sp"
        app:autoSizeMinTextSize="12sp"
        app:autoSizeStepGranularity="2sp" />
    
    <!-- 响应式图片 -->
    <ImageView
        android:id="@+id/responsive_image"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scaleType="centerCrop"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.8"
        app:layout_constraintHeight_default="percent"
        app:layout_constraintHeight_percent="0.4"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/responsive_text" />
</androidx.constraintlayout.widget.ConstraintLayout>

4.9 辅助功能

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 去除了 padding 的 View,节省空间 -->
    <TextView
        android:id="@+id/content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:padding="16dp"
        android:text="内容"
        app:layout_optimizationLevel="standard"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    
    <!-- 直接画在父 View 上,减少层级 -->
    <View
        android:id="@+id/line"
        android:layout_width="0dp"
        android:layout_height="1dp"
        android:background="@color/divider"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/content"
        app:layout_ignorePadding="true" />
</androidx.constraintlayout.widget.ConstraintLayout>

5. 多屏幕适配方案

5.1 经典方案:Jetpack Fragment + 多栏布局

xml
<!-- res/layout/activity_main.xml (手机,单栏) -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <fragment
        android:id="@+id/list_fragment"
        android:name="com.example.ListFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
xml
<!-- res/layout-sw600dp/activity_main.xml (平板,双栏) -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <fragment
        android:id="@+id/list_fragment"
        android:name="com.example.ListFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/detail_fragment"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
    
    <fragment
        android:id="@+id/detail_fragment"
        android:name="com.example.DetailFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_weight="2"
        app:layout_constraintStart_toEndOf="@id/list_fragment"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

5.2 现代方案:Navigation Component

xml
<!-- res/navigation/nav_graph.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:startDestination="@id/listFragment">
    
    <fragment
        android:id="@+id/listFragment"
        android:name="com.example.ListFragment"
        android:label="List" >
        <action
            android:id="@+id/action_list_to_detail"
            app:destination="@id/detailFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>
    
    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.DetailFragment"
        android:label="Detail" />
</navigation>

5.3 Material Design 自适应布局

kotlin
// Material 3 提供自适应组件
class AdaptiveActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 根据屏幕大小选择布局
        val layout = when {
            isTablet() -> R.layout.activity_main_tablet
            isPhoneLandscape() -> R.layout.activity_main_phone_landscape
            else -> R.layout.activity_main_phone
        }
        
        setContentView(layout)
    }
    
    private fun isTablet(): Boolean {
        return resources.configuration.smallestScreenWidthDp >= 600
    }
    
    private fun isPhoneLandscape(): Boolean {
        return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
               resources.configuration.smallestScreenWidthDp < 600
    }
}

5.4 响应式网格布局

xml
<!-- 使用 Grid 布局实现响应式网格 -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 小屏幕:单列 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
kotlin
// 动态设置列数
class ResponsiveGridAdapter : RecyclerView.Adapter<ViewHolder>() {
    
    private var spanCount = 1
    
    fun setSpanCount(newSpanCount: Int) {
        spanCount = newSpanCount
    }
    
    // 根据屏幕宽度计算列数
    fun calculateSpanCount(screenWidthDp: Int): Int {
        return when {
            screenWidthDp < 360 -> 1
            screenWidthDp < 480 -> 2
            screenWidthDp < 600 -> 3
            screenWidthDp < 840 -> 4
            else -> 6
        }
    }
}

5.5 响应式图片网格

xml
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/photo_grid"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
    app:spanCount="@integer/grid_span_count" />
xml
<!-- res/values/ints.xml -->
<resources>
    <integer name="grid_span_count">2</integer>
</resources>

<!-- res/values-sw600dp/ints.xml -->
<resources>
    <integer name="grid_span_count">4</integer>
</resources>

<!-- res/values-sw720dp/ints.xml -->
<resources>
    <integer name="grid_span_count">6</integer>
</resources>

5.6 响应式列表与详情

kotlin
/**
 * 响应式列表与详情实现
 */
class MasterDetailActivity : AppCompatActivity() {
    
    private lateinit var listFragment: ListFragment
    private var detailFragment: DetailFragment? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_master_detail)
        
        listFragment = supportFragmentManager
            .findFragmentById(R.id.list_container) as ListFragment
        
        // 平板:显示详情 Fragment
        val detailContainer = findViewById<View>(R.id.detail_container)
        if (detailContainer != null && savedInstanceState == null) {
            detailFragment = DetailFragment()
            supportFragmentManager
                .beginTransaction()
                .add(R.id.detail_container, detailFragment)
                .commit()
        }
        
        listFragment.setDetailCallback { item ->
            showDetail(item)
        }
    }
    
    private fun showDetail(item: Item) {
        if (isTablet()) {
            // 平板:替换详情 Fragment
            detailFragment = DetailFragment.newInstance(item)
            supportFragmentManager
                .beginTransaction()
                .replace(R.id.detail_container, detailFragment)
                .commit()
        } else {
            // 手机:导航到详情 Activity
            startActivity(DetailActivity.newIntent(this, item))
        }
    }
    
    private fun isTablet(): Boolean {
        return resources.configuration.smallestScreenWidthDp >= 600
    }
}

6. 折叠屏适配

6.1 折叠屏特性

┌─────────────────────────────────────────────┐
│              折叠屏设备矩阵                   │
├─────────────────────────────────────────────┤
│ Galaxy Fold          7.6 英寸折叠 6.2 外屏     │
│ Galaxy Z Flip        6.7 英寸折叠 1.1 外屏     │
│ Galaxy Z Fold3       7.6 英寸折叠 6.2 外屏     │
│ Galaxy Z Fold4       7.6 英寸折叠 6.2 外屏     │
│ Huawei Mate Xs       8 英寸折叠 6.6 外屏        │
│ Xiaomi Mix Fold      8 英寸折叠 6.52 外屏       │
│ Oppo Find N          7.1 英寸折叠 5.49 外屏     │
│ Motorola Razr        6.7 英寸折叠 1.9 外屏      │
└─────────────────────────────────────────────┘

6.2 WindowMetricsAPI(推荐)

kotlin
class FoldableActivity : AppCompatActivity() {
    
    private val windowSizeClassDelegate = WindowSizeClassDelegate()
    private val windowMetrics = WindowMetrics()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 监听窗口尺寸变化
        window.addWindowCallback { event ->
            if (event is WindowEvent.CONTENT_RECT_CHANGED) {
                onWindowMetricsChanged()
            }
        }
        
        // 获取当前窗口尺寸
        val windowMetrics = window.currentWindowMetrics
        val bounds = windowMetrics.bounds
        val widthDp = bounds.width().toDp(this)
        val heightDp = bounds.height().toDp(this)
        
        // 根据尺寸调整布局
        adjustLayoutForSize(widthDp, heightDp)
    }
    
    private fun onWindowMetricsChanged() {
        val windowMetrics = window.currentWindowMetrics
        val bounds = windowMetrics.bounds
        
        // 判断是否展开
        val isUnfolded = isScreenUnfolded(windowMetrics)
        
        // 调整布局
        if (isUnfolded) {
            showMultiPaneLayout()
        } else {
            showSinglePaneLayout()
        }
    }
    
    private fun isScreenUnfolded(windowMetrics: WindowMetrics): Boolean {
        val bounds = windowMetrics.bounds
        val widthDp = bounds.width().toDp(this)
        val heightDp = bounds.height().toDp(this)
        
        // 判断是否展开(屏幕面积大于阈值)
        val areaDp = widthDp * heightDp
        return areaDp > 500_000 // 大约 700dp x 700dp
    }
    
    private fun adjustLayoutForSize(widthDp: Int, heightDp: Int) {
        // 根据屏幕尺寸调整布局
        when {
            widthDp > 600 -> {
                // 大屏布局
            }
            widthDp > 480 -> {
                // 中屏布局
            }
            else -> {
                // 小屏布局
            }
        }
    }
    
    private fun Int.toDp(context: Context): Int {
        return (this / context.resources.displayMetrics.density).toInt()
    }
}

6.3 Foldables 库

kotlin
// 添加依赖
// implementation "com.google.android.fhir:foldables:1.1.0"

class FoldableActivity : AppCompatActivity() {
    
    private lateinit var windowSizeClassDelegate: WindowSizeClassDelegate
    private lateinit var windowFeatureStateUpdateCallback: WindowFeatureStateUpdateCallback
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 监听铰链状态
        windowFeatureStateUpdateCallback = object : WindowFeatureStateUpdateCallback {
            override fun onWindowFeatureStateChanged(windowFeatureStates: List<WindowFeatureState>?) {
                windowFeatureStates?.forEach { state ->
                    if (state.isHinge) {
                        onHingeStateChanged(state.position)
                    }
                }
            }
        }
        
        WindowFeatureManagerCompat.getInstance(this)
            .addWindowFeatureStateUpdateCallback(windowFeatureStateUpdateCallback)
        
        // 根据折叠状态调整布局
        val windowState = WindowStateCalculator.getInstance(this).calculateWindowState()
        when (windowState) {
            WindowState.FOLDED -> {
                // 折叠状态
                setContentView(R.layout.activity_folded)
            }
            WindowState.UNFOLDED -> {
                // 展开状态
                setContentView(R.layout.activity_unfolded)
            }
            WindowState.MULTIPLE_DISPLAYS -> {
                // 多屏状态
                setContentView(R.layout.activity_multi_display)
            }
        }
    }
    
    private fun onHingeStateChanged(position: Rect) {
        // 铰链位置变化时的处理
        // position 表示铰链在屏幕上的位置
        updateLayoutForHinge(position)
    }
    
    private fun updateLayoutForHinge(hingePosition: Rect) {
        // 根据铰链位置调整布局
        // 避免重要内容被铰链遮挡
        val hingeCenterX = hingePosition.left + hingePosition.width() / 2
        val hingeCenterY = hingePosition.top + hingePosition.height() / 2
        
        // 调整内容布局,避免铰链区域
        adjustContentAvoidHinge(hingePosition)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        WindowFeatureManagerCompat.getInstance(this)
            .removeWindowFeatureStateUpdateCallback(windowFeatureStateUpdateCallback)
    }
}

6.4 连续布局适配

xml
<!-- res/layout/activity_main.xml (基础布局) -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">
    
    <!-- 铰链感知容器 -->
    <androidx.core.widget.NestedScrollView
        android:id="@+id/scroll_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">
        
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            
            <ImageView
                android:id="@+id/header_image"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:scaleType="centerCrop"
                android:importantForAccessibility="no" />
            
            <TextView
                android:id="@+id/title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="24sp"
                android:textStyle="bold"
                android:padding="16dp" />
            
            <TextView
                android:id="@+id/content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:lineSpacingExtra="4dp"
                android:padding="16dp" />
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
kotlin
// 动态调整布局以避免铰链
class HingeAwareLayoutManager : LayoutManager() {
    
    private var hingeRect: Rect? = null
    
    fun setHingeRect(rect: Rect?) {
        hingeRect = rect
    }
    
    override fun onLayoutChildren(recycler: RecyclerView, state: RecyclerView.State) {
        // 避免在铰链区域放置重要内容
        hingeRect?.let { hinge ->
            // 跳过铰链区域
            // 实现具体的布局逻辑
        }
        
        // 调用标准的布局逻辑
        super.onLayoutChildren(recycler, state)
    }
}

6.5 多窗口模式

kotlin
class MultiWindowActivity : AppCompatActivity() {
    
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        
        // 检查是否进入多窗口模式
        val isInMultiWindowMode = isInMultiWindowMode
        
        if (isInMultiWindowMode) {
            // 多窗口模式:缩小布局
            setupCompactLayout()
        } else {
            // 正常模式:完整布局
            setupNormalLayout()
        }
    }
    
    private fun setupCompactLayout() {
        // 多窗口模式下的布局调整
        // 减少动画、简化 UI、优化性能
    }
    
    private fun setupNormalLayout() {
        // 正常模式下的布局
    }
}

6.6 连续可用性测试

kotlin
// 测试折叠状态变化
@Test
fun testFoldUnfold() {
    // 1. 初始状态:折叠
    val initialState = getInitialState()
    assertThat(initialState.isFolded).isTrue()
    
    // 2. 模拟展开
    simulateUnfold()
    
    // 3. 验证展开状态
    val unfoldedState = getUnfoldedState()
    assertThat(unfoldedState.isFolded).isFalse()
    
    // 4. 验证 UI 状态保持
    assertThat(listFragment.itemCount).isEqualTo(initialItemCount)
    assertThat(detailFragment.data).isNotNull()
    
    // 5. 模拟重新折叠
    simulateFold()
    
    // 6. 验证折叠状态
    val foldedState = getFoldedState()
    assertThat(foldedState.isFolded).isTrue()
}

7. 横竖屏适配

7.1 AndroidManifest 配置

xml
<manifest>
    <!-- 固定竖屏 -->
    <activity
        android:name=".PortraitActivity"
        android:screenOrientation="portrait" />
    
    <!-- 固定横屏 -->
    <activity
        android:name=".LandscapeActivity"
        android:screenOrientation="landscape" />
    
    <!-- 跟随系统设置(默认) -->
    <activity
        android:name=".Activity"
        android:screenOrientation="unspecified" />
    
    <!-- 自动旋转 -->
    <activity
        android:name=".Activity"
        android:screenOrientation="sensor" />
    
    <!-- 禁止在配置变化时重建 Activity -->
    <activity
        android:name=".Activity"
        android:configChanges="orientation|screenSize|keyboardHidden" />
</manifest>

7.2 处理配置变化

kotlin
class ConfigAwareActivity : AppCompatActivity() {
    
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        
        // 检查是否是横竖屏切换
        if (newConfig.orientation != resources.configuration.orientation) {
            // 横竖屏切换处理
            
            val isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
            
            // 调整布局
            adjustLayoutForOrientation(isLandscape)
            
            // 保持状态
            preserveState(isLandscape)
        }
    }
    
    private fun adjustLayoutForOrientation(isLandscape: Boolean) {
        // 根据方向调整布局
        
        if (isLandscape) {
            // 横屏:使用更宽的布局
            setContentView(R.layout.activity_main_landscape)
        } else {
            // 竖屏:使用标准的布局
            setContentView(R.layout.activity_main_portrait)
        }
    }
    
    private fun preserveState(isLandscape: Boolean) {
        // 保存和恢复状态
        // 保持滚动位置、选中状态等
    }
}

7.3 响应式横竖屏布局

xml
<!-- res/layout/activity_main.xml (竖屏) -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <ImageView
        android:id="@+id/header"
        android:layout_width="0dp"
        android:layout_height="200dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/header"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
xml
<!-- res/layout-land/activity_main.xml (横屏) -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <ImageView
        android:id="@+id/header"
        android:layout_width="200dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
    
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/header"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

7.4 自适应横竖屏布局(推荐)

xml
<!-- res/layout/activity_main.xml (横竖屏通用) -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <ImageView
        android:id="@+id/header"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="200:100"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.8" />
    
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/header"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    
    <!-- 横屏时显示额外面板 -->
    <FrameLayout
        android:id="@+id/side_panel"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/list"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
kotlin
// 动态调整侧边栏可见性
class AdaptiveActivity : AppCompatActivity() {
    
    private lateinit var sidePanel: View
    
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        
        val isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
        
        sidePanel.visibility = if (isLandscape) {
            View.VISIBLE
        } else {
            View.GONE
        }
    }
}

7.5 横竖屏数据同步

kotlin
/**
 * 横竖屏切换时保持数据同步
 */
class SyncAdapterActivity : AppCompatActivity() {
    
    private lateinit var viewModel: SharedViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 使用 ViewModel 共享数据
        viewModel = ViewModelProvider(this)[SharedViewModel::class.java]
        
        // 观察数据变化
        viewModel.uiState.observe(this) { state ->
            updateUI(state)
        }
    }
    
    private fun updateUI(state: UIState) {
        // 根据状态更新 UI
        // 横竖屏切换时数据自动同步
    }
    
    // 使用 SharedViewModel 在横竖屏切换时保持数据
    class SharedViewModel : ViewModel() {
        private val _uiState = MutableLiveData<UIState>()
        val uiState = _uiState.asLiveData()
        
        fun loadData() {
            // 加载数据
        }
    }
}

8. Material 3 自适应组件

8.1 Material 3 特性

Material 3 引入了动态颜色自适应组件,支持根据屏幕尺寸自动调整 UI。

8.2 动态颜色

xml
<!-- themes.xml -->
<resources>
    <style name="Theme.MyApp" parent="Theme.Material3.DayNight">
        <!-- 启用动态颜色 -->
        <item name="colorPrimary">@color/md_theme_primary</item>
        <item name="colorOnPrimary">@color/md_theme_onPrimary</item>
        <item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
        <item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
        <!-- 其他颜色 -->
    </style>
</resources>
kotlin
// 获取动态颜色
val colorScheme = MaterialTheme.colorScheme
val primaryColor = colorScheme.primary
val onPrimaryColor = colorScheme.onPrimary

8.3 自适应导航组件

kotlin
// NavigationBar(手机竖屏)
<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/navigation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:menu="@menu/bottom_nav_menu" />

// NavigationRail(手机横屏/小平板)
<com.google.android.material.navigationrail.NavigationRailView
    android:id="@+id/navigation_rail"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    app:menu="@menu/bottom_nav_menu" />

// NavigationDrawer(大平板)
<com.google.android.material.navigation.NavigationView
    android:id="@+id/navigation_drawer"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    app:menu="@menu/bottom_nav_menu" />

8.4 响应式 NavigationComponent

kotlin
class AdaptiveNavigationActivity : AppCompatActivity() {
    
    private lateinit var bottomNav: BottomNavigationView
    private lateinit var navRail: NavigationRailView
    private lateinit var navDrawer: NavigationView
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 根据屏幕尺寸选择导航组件
        val smallestWidth = resources.configuration.smallestScreenWidthDp
        
        when {
            smallestWidth < 600 -> {
                // 手机:底部导航
                bottomNav.visibility = View.VISIBLE
                navRail.visibility = View.GONE
                navDrawer.visibility = View.GONE
            }
            smallestWidth < 840 -> {
                // 平板:侧边导航栏
                bottomNav.visibility = View.GONE
                navRail.visibility = View.VISIBLE
                navDrawer.visibility = View.GONE
            }
            else -> {
                // 大平板:抽屉导航
                bottomNav.visibility = View.GONE
                navRail.visibility = View.GONE
                navDrawer.visibility = View.VISIBLE
            }
        }
    }
}

8.5 自适应卡片

xml
<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardElevation="4dp"
    app:cardCornerRadius="8dp">
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">
        
        <ImageView
            android:id="@+id/image"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:scaleType="centerCrop" />
        
        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textStyle="bold"
            android:layout_marginTop="16dp" />
        
        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:layout_marginTop="8dp" />
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>

8.6 自适应表单

xml
<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <com.google.android.material.textfield.TextInputLayout
        style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:hint="用户名">
        
        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </com.google.android.material.textfield.TextInputLayout>
    
    <!-- 平板:并排显示 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_margin="16dp">
        
        <com.google.android.material.textfield.TextInputLayout
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginEnd="8dp"
            android:hint="用户名">
            
            <com.google.android.material.textfield.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </com.google.android.material.textfield.TextInputLayout>
        
        <com.google.android.material.textfield.TextInputLayout
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="8dp"
            android:hint="密码">
            
            <com.google.android.material.textfield.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>
    </LinearLayout>
</androidx.core.widget.NestedScrollView>

9. Jetpack Compose 响应式

9.1 Compose 响应式优势

kotlin
// Compose 声明式 UI
@Composable
fun ResponsiveList() {
    val items = remember { mutableStateListOf("Item 1", "Item 2", "Item 3") }
    
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        items(items) { item ->
            ListItem(item)
        }
    }
}

9.2 响应式尺寸检测

kotlin
@Composable
fun ResponsiveLayout() {
    val layoutWidth = remember { MutableState(0) }
    val density = LocalDensity.current.currentDensity
    
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                layoutWidth.value = size.width
            }
    ) {
        when {
            layoutWidth > 800 -> LargeScreenLayout()
            layoutWidth > 600 -> MediumScreenLayout()
            else -> SmallScreenLayout()
        }
    }
}

9.3 自适应网格

kotlin
@Composable
fun ResponsiveGrid(items: List<String>) {
    val density = LocalDensity.current
    val layoutWidth = remember { MutableState(0) }
    
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                layoutWidth.value = size.width
            }
    ) {
        val columns = when {
            layoutWidth > 800 -> 4
            layoutWidth > 600 -> 3
            layoutWidth > 400 -> 2
            else -> 1
        }
        
        LazyVerticalGrid(
            columns = GridColumns.Fixed(columns),
            modifier = Modifier.fillMaxSize()
        ) {
            items(items) { item ->
                GridItem(item)
            }
        }
    }
}

9.4 响应式导航

kotlin
@Composable
fun AdaptiveNavScaffold(
    navController: NavController
) {
    val screenWidth = remember { MutableState(0f) }
    
    Scaffold(
        modifier = Modifier
            .onSizeChanged { size ->
                screenWidth.value = size.width.toFloat()
            }
    ) {
        val showNavigationRail = screenWidth.value > 600f
        val showNavigationDrawer = screenWidth.value > 800f
        
        when {
            showNavigationDrawer -> {
                NavigationDrawerScaffold(
                    drawerContent = { NavigationDrawerContent(navController) }
                ) {
                    Content()
                }
            }
            showNavigationRail -> {
                NavigationRailScaffold(
                    navRail = { NavigationRailContent(navController) }
                ) {
                    Content()
                }
            }
            else -> {
                BottomNavigationScaffold(
                    bottomBar = { BottomNavigationBar(navController) }
                ) {
                    Content()
                }
            }
        }
    }
}

9.5 响应式列表详情

kotlin
@Composable
fun MasterDetailScreen(
    items: List<Item>,
    onItemSelected: (Item) -> Unit
) {
    val screenWidth = remember { MutableState(0f) }
    
    val showDetail = screenWidth.value > 600f
    
    Row(
        modifier = Modifier
            .fillMaxSize()
            .onSizeChanged { size ->
                screenWidth.value = size.width.toFloat()
            }
    ) {
        // 列表
        Box(
            modifier = Modifier
                .weight(1f)
                .fillMaxHeight()
        ) {
            LazyColumn {
                items(items) { item ->
                    ListItem(
                        item = item,
                        onClick = { onItemSelected(item) },
                        modifier = Modifier.fillMaxWidth()
                    )
                }
            }
        }
        
        // 详情(大屏显示)
        if (showDetail) {
            Box(
                modifier = Modifier
                    .weight(2f)
                    .fillMaxHeight()
            ) {
                // 详情内容
            }
        }
    }
}

10. 性能优化

10.1 布局层级优化

kotlin
// 使用 Layout Inspector 检查布局层级
// 目标:保持层级在 12 层以内

// ❌ 嵌套过多
<LinearLayout>
    <LinearLayout>
        <LinearLayout>
            <TextView />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

// ✅ 使用 ConstraintLayout
<ConstraintLayout>
    <TextView
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</ConstraintLayout>

10.2 资源加载优化

kotlin
// ✅ 使用不同的资源限定符
// 避免在代码中判断屏幕尺寸

// res/values/dimens.xml
<dimen name="margin_small">8dp</dimen>

// res/values-sw600dp/dimens.xml
<dimen name="margin_small">16dp</dimen>

10.3 图片适配优化

kotlin
// ✅ 使用不同的图片资源
// res/drawable/bg.png
// res/drawable-hdpi/bg.png
// res/drawable-xhdpi/bg.png

// ✅ 使用 Vector Drawable
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="@android:color/black"
        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
</vector>

10.4 动画优化

kotlin
// ✅ 大屏减少动画复杂度
val isLargeScreen = resources.configuration.smallestScreenWidthDp >= 600

if (!isLargeScreen) {
    // 小屏:简化动画
    view.animate()
        .alpha(1f)
        .setDuration(200)
        .start()
} else {
    // 大屏:完整动画
    view.animate()
        .alpha(1f)
        .scaleX(1.1f)
        .scaleY(1.1f)
        .setDuration(300)
        .start()
}

11. 面试考点汇总

11.1 基础概念题

Q1: dp 和 px 有什么区别?为什么推荐使用 dp?

考点: 度量单位理解

参考答案:

  • px(Pixel):物理像素,受屏幕密度影响
  • dp(Density-independent Pixel):密度无关像素

转换公式:

1dp = 1px at 160dpi
dp = px / (density / 160)

为什么推荐 dp:

  1. 在不同密度设备上保持视觉大小一致
  2. 避免高密度设备上元素过小,低密度设备上元素过大
  3. Android 推荐使用 dp 定义布局和 margin/padding

Q2: 什么是资源限定符?常见的限定符有哪些?

考点: 资源适配机制

参考答案:

资源限定符用于为不同配置提供不同的资源。

常见限定符:

限定符示例用途
最小宽度sw600dp平板适配
屏幕方向-land, -port横竖屏
夜间模式-night深色主题
语言-zh, -en多语言
密度-mdpi, -xhdpi图片资源
屏幕尺寸-large, -xlarge屏幕分类

11.2 技术实现题

Q3: ConstraintLayout 相比 LinearLayout 有什么优势?

考点: 布局性能

参考答案:

特性LinearLayoutConstraintLayout
层级多层嵌套扁平化
性能较低
灵活性一般优秀
响应式

优势:

  1. 扁平化布局:减少视图层级,提升性能
  2. 强大的约束系统:Guideline、Barrier、Chain
  3. 百分比布局:支持响应式百分比
  4. 宽高比支持layout_constraintDimensionRatio
  5. 设计器支持:Android Studio 可视化编辑

Q4: 如何实现响应式的多栏列表布局?

考点: 响应式适配

参考答案:

kotlin
// 方式 1:使用资源限定符
// res/values/ints.xml: <integer name="span_count">2</integer>
// res/values-sw600dp/ints.xml: <integer name="span_count">4</integer>

// 方式 2:动态计算列数
fun calculateSpanCount(screenWidthDp: Int): Int {
    return when {
        screenWidthDp < 480 -> 2
        screenWidthDp < 600 -> 3
        screenWidthDp < 840 -> 4
        else -> 6
    }
}

// 方式 3:使用 Compose 响应式
val columns = GridColumns.Fixed(
    (screenWidth / 160).coerceIn(2, 6)
)

11.3 场景设计题

Q5: 设计一个支持手机、平板、折叠屏的应用布局

考点: 多设备适配

参考答案:

┌─────────────────────────────────────────────┐
│              响应式布局设计                  │
├──────────────┬──────────────┬───────────────┤
│   手机       │   平板       │   折叠屏      │
├──────────────┼──────────────┼───────────────┤
│ 单栏布局     │ 双栏布局     │ 双栏布局      │
│ BottomNav    │ NavRail      │ 铰链感知      │
│ 垂直滚动     │ 固定高度     │ 展开自适应    │
│              │              │ 分屏支持      │
└──────────────┴──────────────┴───────────────┘

实现方案:
1. 使用 sw<N>dp 限定符区分设备
2. Fragment + MasterDetail 模式
3. WindowMetricsAPI 监听折叠状态
4. 使用 Material 3 自适应组件
5. ViewModel 共享状态

Q6: 横竖屏切换时如何保持 UI 状态?

考点: 状态保持

参考答案:

方案 1:ViewModel(推荐)

kotlin
class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UIState>()
    val uiState = _uiState.asLiveData()
}

方案 2:onSaveInstanceState

kotlin
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putInt("scroll_position", recyclerView.position)
}

方案 3:配置变化处理

xml
<activity android:configChanges="orientation|screenSize" />

11.4 手撕代码题

Q7: 实现一个响应式的图片网格

kotlin
@Composable
fun ResponsiveImageGrid(images: List<String>) {
    val density = LocalDensity.current
    val screenWidthDp = remember { mutableStateOf(0) }
    
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                screenWidthDp.value = with(density) { size.width.toDp().toInt() }
            }
    ) {
        val spanCount = when {
            screenWidthDp.value < 360 -> 1
            screenWidthDp.value < 480 -> 2
            screenWidthDp.value < 600 -> 3
            screenWidthDp.value < 840 -> 4
            else -> 6
        }
        
        LazyVerticalGrid(
            columns = GridColumns.Fixed(spanCount),
            contentPadding = PaddingValues(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(images) { imageUrl ->
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .aspectRatio(1f)
                ) {
                    AsyncImage(
                        model = imageUrl,
                        contentDescription = null,
                        modifier = Modifier.fillMaxSize(),
                        contentScale = ContentScale.Crop
                    )
                }
            }
        }
    }
}

Q8: 实现一个折叠屏感知列表

kotlin
class FoldableActivity : AppCompatActivity() {
    
    private val windowFeatureCallback = object : WindowFeatureStateUpdateCallback {
        override fun onWindowFeatureStateChanged(states: List<WindowFeatureState>?) {
            states?.forEach { state ->
                if (state.isHinge) {
                    adjustLayoutForHinge(state.position)
                }
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        WindowFeatureManagerCompat.getInstance(this)
            .addWindowFeatureStateUpdateCallback(windowFeatureCallback)
        
        val windowState = WindowStateCalculator.getInstance(this)
            .calculateWindowState()
        
        when (windowState) {
            WindowState.FOLDED -> setContentView(R.layout.folded)
            WindowState.UNFOLDED -> setContentView(R.layout.unfolded)
            else -> setContentView(R.layout.default_layout)
        }
    }
    
    private fun adjustLayoutForHinge(hingeRect: Rect) {
        // 避免内容被铰链遮挡
        val list = findViewById<RecyclerView>(R.id.list)
        list.setPadding(
            0, 0, 0, 
            hingeRect.height() + 32
        )
    }
    
    override fun onDestroy() {
        super.onDestroy()
        WindowFeatureManagerCompat.getInstance(this)
            .removeWindowFeatureStateUpdateCallback(windowFeatureCallback)
    }
}

总结

核心知识点

  1. 度量单位

    • dp:密度无关像素
    • sp:字体缩放像素
    • 百分比和权重
  2. 资源限定符

    • sw<N>dp:最小宽度(推荐)
    • 屏幕方向、语言、夜间模式
  3. ConstraintLayout

    • Guideline、Barrier、Chain
    • 百分比布局、宽高比
    • 响应式文本
  4. 多屏幕适配

    • Fragment + 多栏布局
    • 动态计算列数
  5. 折叠屏适配

    • WindowMetricsAPI
    • Foldables 库
    • 铰链感知
  6. Material 3

    • 动态颜色
    • 自适应组件
    • 响应式导航

最佳实践

  • ✅ 使用 ConstraintLayout 减少层级
  • ✅ 使用资源限定符而非代码判断
  • ✅ 使用 ViewModel 保持状态
  • ✅ 测试多种屏幕尺寸
  • ✅ 使用 Material 3 自适应组件

面试准备

  1. 掌握 dp、sp 等度量单位
  2. 理解资源限定符优先级
  3. 熟悉 ConstraintLayout 高级特性
  4. 了解折叠屏适配方案
  5. 有实际的多设备适配经验

参考链接: