Appearance
09_响应式布局
目录
- 响应式布局概述
- Android 多屏幕适配挑战
- 资源限定符详解
- ConstraintLayout 响应式设计
- 多屏幕适配方案
- 折叠屏适配
- 横竖屏适配
- Material 3 自适应组件
- Jetpack Compose 响应式
- 性能优化
- 面试考点汇总
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 │ 分屏 │ │
└──────────────┴──────────────┴──────────────┴───────────┘碎片化挑战:
- 尺寸多样:从 3.5 寸到 13 寸,超过 10 种主流尺寸
- 密度各异:mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi
- 比例复杂:16:9、18:9、19.5:9、20:9
- 形态创新:折叠屏、卷轴屏、分屏
1.3 响应式布局的演变
传统布局 (2008-2014)
├─ 固定尺寸 (dp)
├─ 简单百分比
└─ 资源限定符
↓ (碎片化加剧)
相对布局 (2014-2017)
├─ RelativeLayout
├─ 嵌套布局
└─ 手动适配
↓ (复杂度爆炸)
约束布局 (2017-2020)
├─ ConstraintLayout
├─ 链和偏置
├─ Guideline 和 Barrier
└─ 响应式约束
↓ (折叠屏时代)
现代响应式 (2020-至今)
├─ Material 3 Adaptive
├─ Jetpack Compose
├─ Foldables API
└─ Window Metrics API2. 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 |
|---|---|---|---|
| mdpi | 160 | 1.0x | 1px |
| hdpi | 240 | 1.5x | 1.5px |
| xhdpi | 320 | 2.0x | 2px |
| xxhdpi | 480 | 3.0x | 3px |
| xxxhdpi | 640 | 4.0x | 4px |
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.xmlxml
<!-- 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.xmlxml
<!-- 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.xmlxml
<!-- 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.xml3.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.smallestScreenWidthDp3.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.onPrimary8.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:
- 在不同密度设备上保持视觉大小一致
- 避免高密度设备上元素过小,低密度设备上元素过大
- Android 推荐使用 dp 定义布局和 margin/padding
Q2: 什么是资源限定符?常见的限定符有哪些?
考点: 资源适配机制
参考答案:
资源限定符用于为不同配置提供不同的资源。
常见限定符:
| 限定符 | 示例 | 用途 |
|---|---|---|
| 最小宽度 | sw600dp | 平板适配 |
| 屏幕方向 | -land, -port | 横竖屏 |
| 夜间模式 | -night | 深色主题 |
| 语言 | -zh, -en | 多语言 |
| 密度 | -mdpi, -xhdpi | 图片资源 |
| 屏幕尺寸 | -large, -xlarge | 屏幕分类 |
11.2 技术实现题
Q3: ConstraintLayout 相比 LinearLayout 有什么优势?
考点: 布局性能
参考答案:
| 特性 | LinearLayout | ConstraintLayout |
|---|---|---|
| 层级 | 多层嵌套 | 扁平化 |
| 性能 | 较低 | 高 |
| 灵活性 | 一般 | 优秀 |
| 响应式 | 弱 | 强 |
优势:
- 扁平化布局:减少视图层级,提升性能
- 强大的约束系统:Guideline、Barrier、Chain
- 百分比布局:支持响应式百分比
- 宽高比支持:
layout_constraintDimensionRatio - 设计器支持: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)
}
}总结
核心知识点
度量单位
- dp:密度无关像素
- sp:字体缩放像素
- 百分比和权重
资源限定符
- sw<N>dp:最小宽度(推荐)
- 屏幕方向、语言、夜间模式
ConstraintLayout
- Guideline、Barrier、Chain
- 百分比布局、宽高比
- 响应式文本
多屏幕适配
- Fragment + 多栏布局
- 动态计算列数
折叠屏适配
- WindowMetricsAPI
- Foldables 库
- 铰链感知
Material 3
- 动态颜色
- 自适应组件
- 响应式导航
最佳实践
- ✅ 使用 ConstraintLayout 减少层级
- ✅ 使用资源限定符而非代码判断
- ✅ 使用 ViewModel 保持状态
- ✅ 测试多种屏幕尺寸
- ✅ 使用 Material 3 自适应组件
面试准备
- 掌握 dp、sp 等度量单位
- 理解资源限定符优先级
- 熟悉 ConstraintLayout 高级特性
- 了解折叠屏适配方案
- 有实际的多设备适配经验
参考链接: