屏幕相关的概念
屏幕尺寸
定义
- 屏幕尺寸指的是屏幕对角线的长度,单位是英寸(inch),1英寸=2.54厘米。
- 常见的屏幕尺寸有4英寸、4.5英寸、5.0英寸、5.2英寸、5.4英寸、5.99英寸、6.0英寸、6.2英寸等。
物理意义
- 屏幕尺寸是物理上的大小,与屏幕的分辨率、像素密度等参数无关。
- 它主要影响用户手持设备的舒适度和观看屏幕内容的视野范围。
屏幕分辨率
定义
- 屏幕分辨率指的是屏幕的像素点总数,一般用屏幕宽的像素点数乘以屏幕高的像素点数来表示,单位是px(pixel),1px=1个像素点。
- 例如,1080x1920表示屏幕在纵向上有1920个像素点,横向上有1080个像素点。
影响因素
- 屏幕分辨率的高低决定了屏幕能够显示的像素点数量,从而影响图像的清晰度和细节表现。
- 分辨率越高,屏幕能够显示的像素点数量越多,图像越清晰。
常见分辨率
- Android手机屏幕常见的分辨率有480x800、720x1280、1080x1920等。此外,还有更高分辨率的屏幕,如2K(2560x1440)和4K(3840x2160)屏幕。
- UI设计师的设计图会以px作为统一的计量单位。
屏幕像素密度
定义
- 屏幕像素密度(PPI,Pixels Per Inch)指的是每英寸长度上包含的像素点的数量。
- 以dpi为单位,Dots Per Inch。
- 它是屏幕尺寸和屏幕分辨率的函数,计算公式为:PPI=√(宽度像素数²+高度像素数²) / 屏幕对角线英寸数。
意义
- 屏幕像素密度越高,屏幕显示的图像越细腻、越清晰。它反映了屏幕显示图像的精细程度。
分类
密度类型 | 缩写 | 像素密度范围(dpi) | 比例因子 |
---|---|---|---|
低密度 | ldpi | ~120dpi | 0.75x |
中密度 | mdpi | ~160dpi | 1x |
高密度 | hdpi | ~240dpi | 1.5x |
超高密度 | xhdpi | ~320dpi | 2x |
超超高密度 | xxhdpi | ~480dpi | 3x |
超超超高密度 | xxxhdpi | ~640dpi | 4x |
密度无关像素
定义
- density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关。可以保证在不同屏幕像素密度的设备上显示相同的效果。
比例因子
- 公式:dpi/160。
- 如低密度资源(120dpi),比例因子是0.75。
像素密度与单位转换
常用的单位有:
- px(像素):屏幕上的实际像素点。
- dp(密度无关像素):与像素密度无关的单位,1dp在不同设备上会转换为不同的px值。
- sp(缩放无关像素):类似于dp,但主要用于字体大小,会受用户字体设置的影响。
单位转换公式
- px = dp * (dpi / 160)
例如,在xhdpi设备上(320dpi),1dp = 2px。 - dp = px / (dpi / 160)
例如,在xxhdpi设备上(480dpi),48px = 16dp。
几种屏幕适配方案
限定符
尺寸(Size)限定符
尺寸限定符允许你为不同的屏幕尺寸提供不同的布局文件。常用的尺寸限定符包括:
- small:小屏幕设备
- normal:中等屏幕设备
- large:大屏幕设备
- xlarge:超大屏幕设备
示例
在res/layout目录下创建不同的布局文件夹:
res/layout/main.xml # 默认布局
res/layout-small/main.xml # 小屏幕设备
res/layout-large/main.xml # 大屏幕设备
res/layout-xlarge/main.xml # 超大屏幕设备
使用最小宽度(Smallest-width)限定符
最小宽度限定符(sw)允许你根据设备的最小宽度(以dp为单位)提供不同的布局文件。最小宽度是指屏幕的宽度或高度中的较小值。
示例
在res/layout目录下创建不同的布局文件夹:
res/layout/main.xml # 默认布局
res/layout-sw600dp/main.xml # 最小宽度为600dp的设备(如7英寸平板)
res/layout-sw720dp/main.xml # 最小宽度为720dp的设备(如10英寸平板)
屏幕方向(Orientation)限定符
屏幕方向限定符允许你为横屏和竖屏提供不同的布局文件。
示例
在res/layout目录下创建不同的布局文件夹:
res/layout/main.xml # 默认布局
res/layout-port/main.xml # 竖屏布局
res/layout-land/main.xml # 横屏布局
总体示例
res/layout/main.xml # 默认布局(手机竖屏)
res/layout-sw600dp/main.xml # 7英寸平板竖屏)
res/layout-sw720dp/main.xml # 10英寸平板竖屏)
res/layout-port/main.xml # 手机竖屏)
res/layout-land/main.xml # 手机横屏)
res/layout-sw600dp-port/main.xml # 7英寸平板竖屏)
res/layout-sw600dp-land/main.xml # 7英寸平板横屏)
res/layout-sw720dp-port/main.xml # 10英寸平板竖屏)
res/layout-sw720dp-land/main.xml # 10英寸平板横屏)
布局别名(Layout Aliases)
定义
- 布局别名是一种通过XML文件定义布局引用的方式,允许开发者根据不同的条件(如屏幕尺寸、方向等)动态选择不同的布局文件。
- 它的核心思想是复用布局资源,避免为不同条件重复编写相同的布局文件。
使用场景
- 当多个布局文件内容相似,但需要根据屏幕尺寸或方向进行微调时。
- 当需要为不同设备提供不同的布局,但部分布局可以复用时。
实现步骤
定义默认布局
- 在res/layout目录下创建默认布局文件,例如main_default.xml。
<!-- res/layout/main_default.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Default Layout" />
</LinearLayout>
定义其他布局
- 在res/layout目录下创建其他布局文件,例如main_tablet.xml(用于平板设备)。
<!-- res/layout/main_tablet.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tablet Layout" />
</LinearLayout>
创建布局别名
- 在res/values目录下创建layouts.xml文件,定义布局别名。
<!-- res/values/layouts.xml -->
<resources>
<item name="main_layout" type="layout">@layout/main_default</item>
</resources>
- 在res/values-sw600dp目录下创建layouts.xml文件,为平板设备定义布局别名。
<!-- res/values-sw600dp/layouts.xml -->
<resources>
<item name="main_layout" type="layout">@layout/main_tablet</item>
</resources>
使用布局别名
- 在Activity或Fragment中,通过别名引用布局。
setContentView(R.layout.main_layout);
布局选择
几种常见的布局
布局类型 | 特点 | 适用场景 |
---|---|---|
LinearLayout | 线性排列,支持权重 | 简单列表、比例布局 |
RelativeLayout | 相对定位,灵活但性能较差 | 复杂布局 |
ConstraintLayout | 约束关系,性能优越 | 复杂布局,减少嵌套 |
FrameLayout | 堆叠效果 | 单视图、叠加视图 |
TableLayout | 表格形式 | 行列对齐的简单表格 |
GridLayout | 网格形式,支持跨行跨列 | 复杂网格布局 |
今日头条的屏幕适配方案
核心原理
- 今日头条的屏幕适配方案是一种基于动态调整设备密度(density)的适配方法。
- 其核心原理是通过修改系统默认的屏幕密度参数,使得不同分辨率和尺寸的设备能够按照设计图的尺寸比例显示界面元素。
核心公式为:density=设备屏幕总宽度(px)/设计图总宽度(dp)。通过动态计算设备的实际像素宽度与设计图宽度的比例,得到新的密度值(density),并替换系统默认的密度值(通常为160dpi对应的1)。这样,可以确保在不同设备上,控件的实际显示比例与设计图一致。
实现细节
- 设计图尺寸:今日头条通常使用一个固定的设计图尺寸(如375dp宽度)作为基准。
- 动态计算density:在应用启动时,根据设备的实际屏幕宽度和设计图宽度动态计算新的density值。
- 替换系统density:将计算得到的density值替换系统默认的density值,从而影响系统控件和第三方库的显示效果。
- 兼容处理:由于修改density会影响系统控件和第三方库的显示效果,若其设计尺寸与项目不一致,可能导致布局异常。因此,需要进行兼容处理,如使用冷门单位(如pt、mm)作为布局单位,或以Activity为单位自定义设计图尺寸等。
优势与局限
- 优势
- 低成本、低侵入:无需修改布局文件,仅需全局调整density值,适配代码可集中管理。
- 比例一致:控件在不同设备上按设计图比例缩放,避免传统dp适配导致的视觉差异。
- 兼容性强:支持所有Android系统控件及第三方库(需处理冲突)。
- 局限
- 全局影响:修改density会影响系统控件和第三方库的显示效果,可能导致布局异常。
- 高度适配问题:若设备高宽比与设计图差异较大,纵向布局可能需额外处理(如权重布局)。
其他适配方案对比
SmallestWidth限定符方案:需生成多套dimens文件,增加维护成本,但适配更稳定。
使用ConstraintLayout进行适配:ConstraintLayout是一个强大的布局工具,可以通过约束灵活设计界面,适应不同屏幕比例和尺寸。
实现示例
以下是一个基于今日头条屏幕适配方案的核心原理的示例代码实现。
在使用时,只需在Application的onCreate方法中调用DisplayUtil.setCustomDensity(this)即可实现全局的屏幕适配。
该代码通过动态计算并替换DisplayMetrics中的density值,实现屏幕适配:
public class DisplayUtil {
// 设计图的默认宽度(单位:dp)
private static final float DEFAULT_DESIGN_WIDTH_DP = 375f;
private static float sAppDensity;
private static float sAppScaledDensity;
/**
* 初始化适配(在Application中调用)
*/
public static void setCustomDensity(Application application) {
// 获取系统默认的DisplayMetrics
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (sAppDensity == 0) {
sAppDensity = appDisplayMetrics.density;
sAppScaledDensity = appDisplayMetrics.scaledDensity;
// 监听系统字体变化
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sAppScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
// 动态计算新的density值
final float targetDensity = appDisplayMetrics.widthPixels / DEFAULT_DESIGN_WIDTH_DP;
final float targetScaledDensity = targetDensity * (sAppScaledDensity / sAppDensity);
final int targetDensityDpi = (int) (targetDensity * 160);
// 替换全局的DisplayMetrics
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
// 替换Activity的DisplayMetrics(兼容部分机型)
final DisplayMetrics activityDisplayMetrics = application.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}
// ...(其他相关方法)
}