Skip to content

多屏幕适配

字数统计:约 9000 字
难度等级:⭐⭐⭐⭐
面试重要度:⭐⭐⭐⭐


目录

  1. 屏幕适配基础
  2. 布局适配
  3. 图片资源适配
  4. 折叠屏适配
  5. 最佳实践
  6. 面试考点

1. 屏幕适配基础

1.1 屏幕参数

关键概念:
- 屏幕尺寸 (Screen Size) - 对角线长度(英寸)
- 屏幕密度 (Screen Density) - 每英寸像素数 (DPI/PPI)
- 分辨率 (Resolution) - 像素数量(宽 x 高)
- 密度无关像素 (dp/dip) - 与密度无关的单位

1.2 密度分类

密度分类:
- ldpi: 120 dpi (0.75x)
- mdpi: 160 dpi (1.0x) - 基准
- hdpi: 240 dpi (1.5x)
- xhdpi: 320 dpi (2.0x)
- xxhdpi: 480 dpi (3.0x)
- xxxhdpi: 640 dpi (4.0x)

换算公式:
px = dp × (dpi / 160)
dp = px / (dpi / 160)

1.3 尺寸分类

尺寸分类:
- small: 小于 3 英寸
- normal: 3-5 英寸(手机)
- large: 5-7 英寸(平板)
- xlarge: 大于 7 英寸(平板)

1.4 单位使用

kotlin
// ✅ 推荐单位
// dp - 布局尺寸(与密度无关)
// sp - 字体大小(与用户设置相关)
// px - 仅在必要时使用(如 1 像素边框)

// ❌ 避免
// 硬编码 px 值
val width = 100 // 错误

// ✅ 正确
val width = 100.dp // 使用 dp

2. 布局适配

2.1 备用布局

目录结构:
res/
├── layout/                    # 默认布局
│   └── activity_main.xml
├── layout-sw600dp/            # 最小宽度 600dp(7 寸平板)
│   └── activity_main.xml
├── layout-land/               # 横屏布局
│   └── activity_main.xml
├── layout-port/               # 竖屏布局
│   └── activity_main.xml
├── layout-large/              # 大屏幕
│   └── activity_main.xml
└── layout-xlarge/             # 超大屏幕
    └── activity_main.xml

2.2 响应式布局

xml
<!-- 使用 ConstraintLayout 实现响应式 -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 在小屏幕上占满宽度 -->
    <!-- 在大屏幕上居中显示 -->
    <TextView
        android:id="@+id/title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:maxWidth="600dp"
        app:layout_constraintWidth_percent="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2.3 尺寸资源

xml
<!-- res/values/dimens.xml (默认) -->
<resources>
    <dimen name="padding_small">8dp</dimen>
    <dimen name="padding_normal">16dp</dimen>
    <dimen name="padding_large">24dp</dimen>
    <dimen name="text_size_small">12sp</dimen>
    <dimen name="text_size_normal">16sp</dimen>
    <dimen name="text_size_large">20sp</dimen>
</resources>

<!-- res/values-sw600dp/dimens.xml (平板) -->
<resources>
    <dimen name="padding_small">16dp</dimen>
    <dimen name="padding_normal">24dp</dimen>
    <dimen name="padding_large">32dp</dimen>
    <dimen name="text_size_small">14sp</dimen>
    <dimen name="text_size_normal">18sp</dimen>
    <dimen name="text_size_large">24sp</dimen>
</resources>

2.4 代码适配

kotlin
class ScreenAdapter {
    
    // 获取屏幕宽度
    fun getScreenWidth(context: Context): Int {
        return context.resources.displayMetrics.widthPixels
    }
    
    // 获取屏幕高度
    fun getScreenHeight(context: Context): Int {
        return context.resources.displayMetrics.heightPixels
    }
    
    // 获取屏幕密度
    fun getDensity(context: Context): Float {
        return context.resources.displayMetrics.density
    }
    
    // dp 转 px
    fun dpToPx(context: Context, dp: Float): Int {
        return (dp * context.resources.displayMetrics.density + 0.5f).toInt()
    }
    
    // px 转 dp
    fun pxToDp(context: Context, px: Int): Float {
        return px / context.resources.displayMetrics.density
    }
    
    // sp 转 px
    fun spToPx(context: Context, sp: Float): Int {
        return (sp * context.resources.displayMetrics.scaledDensity + 0.5f).toInt()
    }
    
    // 判断是否为平板
    fun isTablet(context: Context): Boolean {
        return context.resources.configuration.smallestScreenWidthDp >= 600
    }
    
    // 判断是否为横屏
    fun isLandscape(context: Context): Boolean {
        return context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
    }
}

// 使用
class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val adapter = ScreenAdapter()
        
        if (adapter.isTablet(this)) {
            // 平板布局
            setContentView(R.layout.activity_main_tablet)
        } else {
            // 手机布局
            setContentView(R.layout.activity_main_phone)
        }
    }
}

3. 图片资源适配

3.1 多密度资源

目录结构:
res/
├── drawable-mdpi/      # 160 dpi (1x)
│   └── icon.png (48x48)
├── drawable-hdpi/      # 240 dpi (1.5x)
│   └── icon.png (72x72)
├── drawable-xhdpi/     # 320 dpi (2x)
│   └── icon.png (96x96)
├── drawable-xxhdpi/    # 480 dpi (3x)
│   └── icon.png (144x144)
└── drawable-xxxhdpi/   # 640 dpi (4x)
    └── icon.png (192x192)

3.2 Vector Drawable

xml
<!-- 使用矢量图(推荐) -->
<!-- res/drawable/ic_star.xml -->
<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/white"
        android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
    
</vector>

// 使用
imageView.setImageResource(R.drawable.ic_star)

3.3 Adaptive Icon

xml
<!-- res/mipmap-anydpi-v26/ic_launcher.xml -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

4. 折叠屏适配

4.1 检测折叠状态

kotlin
class FoldableAdapter {
    
    fun isFoldable(context: Context): Boolean {
        return context.packageManager.hasSystemFeature(
            PackageManager.FEATURE_HARWARE_TYPE_FOLDABLE
        )
    }
    
    fun getFoldState(context: Context): Int {
        val display = context.display ?: return FOLD_STATE_UNKNOWN
        
        return when (display.foldState) {
            Display.FOLD_STATE_CLOSED -> FOLD_STATE_CLOSED
            Display.FOLD_STATE_HALF_OPENED -> FOLD_STATE_HALF_OPENED
            Display.FOLD_STATE_OPENED -> FOLD_STATE_OPENED
            else -> FOLD_STATE_UNKNOWN
        }
    }
    
    companion object {
        const val FOLD_STATE_UNKNOWN = 0
        const val FOLD_STATE_CLOSED = 1
        const val FOLD_STATE_HALF_OPENED = 2
        const val FOLD_STATE_OPENED = 3
    }
}

4.2 响应折叠变化

kotlin
class FoldableActivity : AppCompatActivity() {
    
    private val foldChangeListener = Consumer<Display> { display ->
        when (display.foldState) {
            Display.FOLD_STATE_OPENED -> {
                // 展开状态 - 使用双栏布局
                updateLayoutForExpanded()
            }
            Display.FOLD_STATE_CLOSED -> {
                // 折叠状态 - 使用单栏布局
                updateLayoutForCollapsed()
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 注册监听器
        windowManager.currentWindowMetrics.display
            .addOnFoldChangeListener(foldChangeListener)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        windowManager.currentWindowMetrics.display
            .removeOnFoldChangeListener(foldChangeListener)
    }
}

4.3 跨屏连续性

kotlin
class ContinuityManager {
    
    fun saveState(state: Bundle) {
        // 保存当前状态
        state.putInt("scroll_position", recyclerView.scrollPosition)
        state.putString("current_item", currentItem)
    }
    
    fun restoreState(state: Bundle) {
        // 恢复状态
        val position = state.getInt("scroll_position")
        recyclerView.scrollToPosition(position)
    }
}

5. 最佳实践

5.1 使用 ConstraintLayout

xml
<!-- 响应式布局 -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 自适应宽度 -->
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:maxWidth="600dp"
        app:layout_constraintWidth_percent="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <!-- 链式布局 -->
    <androidx.constraintlayout.widget.Chain
        app:layout_constraintHorizontal_chainStyle="spread" />

</androidx.constraintlayout.widget.ConstraintLayout>

5.2 使用 Fragment

kotlin
// 单栏布局(手机)
class MainActivity : AppCompatActivity() {
    // 只显示一个 Fragment
}

// 双栏布局(平板)
class TabletMainActivity : AppCompatActivity() {
    // 同时显示两个 Fragment
    // Master-Detail 模式
}

5.3 测试多屏幕

kotlin
// Android Studio 模拟器
// 创建不同尺寸的设备:
// - Phone: Pixel 6 (1080x2400, 411x891dp)
// - Tablet: Pixel Tablet (1600x2560, 820x1344dp)
// - Foldable: Pixel Fold (1840x2208, 760x932dp)

// 测试不同方向
// 竖屏 / 横屏

// 测试不同密度
// mdpi / hdpi / xhdpi / xxhdpi

6. 面试考点

6.1 基础概念

Q1: dp 和 px 的区别?

答案要点:
- dp: 密度无关像素,与屏幕密度无关
- px: 物理像素,与屏幕密度相关
- 换算:px = dp × (dpi / 160)
- 建议使用 dp 进行布局

Q2: 如何实现多屏幕适配?

答案要点:
1. 使用 dp/sp 单位
2. 提供备用布局(layout-sw600dp 等)
3. 使用 ConstraintLayout
4. 提供多密度图片资源
5. 使用 Vector Drawable

6.2 实战问题

Q3: 如何判断设备是手机还是平板?

kotlin
fun isTablet(context: Context): Boolean {
    return context.resources.configuration.smallestScreenWidthDp >= 600
}

// 或使用屏幕尺寸
fun isTablet(context: Context): Boolean {
    val config = context.resources.configuration
    return config.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >=
        Configuration.SCREENLAYOUT_SIZE_LARGE
}

Q4: 如何适配折叠屏?

kotlin
// 1. 检测折叠状态
val foldState = display.foldState

// 2. 监听折叠变化
display.addOnFoldChangeListener { display ->
    when (display.foldState) {
        FOLD_STATE_OPENED -> updateLayoutForExpanded()
        FOLD_STATE_CLOSED -> updateLayoutForCollapsed()
    }
}

// 3. 使用响应式布局
// ConstraintLayout + Fragment

6.3 高级问题

Q5: 什么是 smallestWidth?

答案要点:
- smallestScreenWidthDp
- 屏幕最短边的 dp 值
- 用于判断设备类型
- < 600dp: 手机
- >= 600dp: 7 寸平板
- >= 720dp: 10 寸平板

参考资料


本文完,感谢阅读!