在上一篇文章中,我们介绍了Android屏幕适配的基本方法,比如使用限定符资源、图片适配、矢量图等。
感兴趣的朋友,请前往查阅:Android 屏幕适配全攻略(中)-从九宫格到矢量图,揭秘Android多屏幕适配的正确打开方式.
但随着智能手机屏幕形态的不断创新,光靠这些基础做法已经不够,开发者们必须进一步掌握更多专业的适配技巧,才能应对屏幕百变的挑战。本文将重点讲解字体缩放适配、Android 9.0新屏幕支持、异形全面屏等内容,为您分享Android屏幕适配的终极解决之道。
一、字体缩放适配
Android系统为了给用户带来极佳的可访问性体验,允许他们自由调节系统字体大小。所以作为开发者,我们必须确保应用可以完美兼容各种字体缩放场景,既能满足用户需求,也不会破坏界面布局。
1、代码中动态计算、设置字体大小以适应不同的屏幕密度和尺寸
public class MainActivity extends AppCompatActivity {
private static final float BASE_FONT_SIZE = 16f; // 基准字体大小,单位 SP
private static final float BASE_SCREEN_WIDTH = 360f; // 基准屏幕宽度,单位 DP
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取屏幕宽度
int screenWidth = getScreenWidth(this);
// 计算动态字体大小
float dynamicFontSize = calculateDynamicFontSize(screenWidth);
// 设置 TextView 的字体大小
TextView textView = findViewById(R.id.text_view);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, dynamicFontSize);
}
/**
* 获取屏幕宽度(单位:DP)
*/
private static int getScreenWidth(Context context) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (displayMetrics.widthPixels / displayMetrics.density);
}
/**
* 计算动态字体大小(单位:SP)
*/
private static float calculateDynamicFontSize(float screenWidth) {
return BASE_FONT_SIZE * (screenWidth / BASE_SCREEN_WIDTH);
}
}
在这个示例中:
- 我们定义了基准字体大小
BASE_FONT_SIZE
为 16sp,以及基准屏幕宽度BASE_SCREEN_WIDTH
为 360dp。 - 在
onCreate
方法中,我们先获取当前屏幕的宽度(单位为 dp),然后调用calculateDynamicFontSize
方法计算出动态的字体大小(单位为 sp)。 - 最后,我们将计算出的动态字体大小设置到
TextView
上。
calculateDynamicFontSize
方法的实现原理如下:
- 我们假设基准字体大小 16sp 对应了基准屏幕宽度 360dp。
- 当屏幕宽度发生变化时,我们可以按照屏幕宽度的比例来计算出新的字体大小。
- 具体计算公式为:
dynamicFontSize = BASE_FONT_SIZE * (screenWidth / BASE_SCREEN_WIDTH)
,其中screenWidth
为当前屏幕的宽度(单位为 dp)。
通过这种方式,我们可以自动根据屏幕尺寸动态调整字体大小,从而确保文字在不同设备上的显示效果都能较为合理。
2、代码中对文字进行缩放
主要涉及到以下几个方面:
-
使用 sp 作为字体单位 Android 建议使用 sp (scale-independent pixels) 作为字体的单位,因为 sp 会根据用户的字体大小设置进行缩放,能够确保文字大小在不同设备上显示合理。
-
继承 Application 类并重写 attachBaseContext 方法 在应用启动时,可以在自定义的 Application 类中重写
attachBaseContext
方法,在这里对整个应用的字体大小进行缩放适配。 -
使用 TextUtil 工具类 Android 提供了
TextUtil
工具类,可以对文字内容进行缩放。在需要缩放的地方,调用TextUtil.scale(CharSequence source, float proportion)
方法即可。
下面是一个完整的 Java 代码示例:
public class MyApplication extends Application {
private static final float DEFAULT_FONT_SCALE = 1.0f;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 获取当前系统的字体缩放比例
float fontScale = base.getResources().getConfiguration().fontScale;
// 计算需要缩放的比例
float scale = fontScale / DEFAULT_FONT_SCALE;
// 对整个应用的字体进行缩放
ResourcesCompat.getFont(base).setCompatibilityScaling(scale);
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.text_view);
// 对单个 TextView 的文字进行缩放
textView.setText(TextUtils.scale(textView.getText(), 1.2f));
}
}
在这个示例中:
-
自定义的
MyApplication
类重写了attachBaseContext
方法,获取当前系统的字体缩放比例,并对整个应用的字体进行缩放适配。 -
在
MainActivity
中,我们使用TextUtils.scale()
方法对单个TextView
的文字进行了 1.2 倍的缩放。
3、设置布局根元素的autoSizeTextType属性
<LinearLayout
...
app:autoSizeTextType="uniform">
<TextView
android:textSize="16sp"
... />
</LinearLayout>
autoSizeTextType可以让布局内所有文字自动调整大小以完全适应布局高宽。
二、Android 9.0 带来的全新屏幕支持
1、支持异形全面屏
Android 9.0(Pie)正式推出后,Google 在屏幕显示方面做了全面的升级,其中最重要的就是对异形全面屏的支持。
异形全面屏指的是屏幕四周有凹槽或者凸起的屏幕设计,常见于 iPhone X、三星 Galaxy S9 等手机。Android 9.0 提供了一系列 API 和特性来支持这种屏幕形态,包括:
-
DisplayCutout API: 这个 API 可以让开发者获取到屏幕上的"凹槽"区域的信息,包括位置、尺寸等,从而可以更好地适配应用界面。
-
Window Insets: Android 9.0 还提供了 Window Insets 机制,可以让开发者知道当前窗口被系统 UI 遮挡的区域,从而调整界面布局。
-
notch-friendly 导航栏: Android 9.0 的导航栏可以自动适应异形屏幕,当有凹槽时会自动避开。
(1)、使用 DisplayCutout API 适配异形全面屏示例
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取 Window 的 DecorView
final View decorView = getWindow().getDecorView();
// 设置 OnApplyWindowInsets 监听器
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
// 获取屏幕四周的"凹槽"区域
DisplayCutout displayCutout = insets.getDisplayCutout();
if (displayCutout != null) {
// 获取"凹槽"的位置和尺寸
Rect safeInsets = displayCutout.getSafeInsetLeft(),
safeInsets = displayCutout.getSafeInsetTop(),
safeInsets = displayCutout.getSafeInsetRight(),
safeInsets = displayCutout.getSafeInsetBottom();
// 根据"凹槽"信息调整界面布局
adjustLayoutForCutout(decorView, safeInsets);
}
// 返回 insets 信息,以便系统继续处理
return insets;
}
});
}
private void adjustLayoutForCutout(View view, Rect safeInsets) {
// 根据"凹槽"信息调整 view 的内边距
view.setPadding(safeInsets.left, safeInsets.top,
safeInsets.right, safeInsets.bottom);
// 如果 view 是 ViewGroup,则递归调整子 view
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
adjustLayoutForCutout(viewGroup.getChildAt(i), safeInsets);
}
}
}
}
在这个示例中:
- 我们为
Window
的DecorView
设置了OnApplyWindowInsetsListener
监听器,在这个监听器中获取屏幕"凹槽"的信息。 - 我们通过
DisplayCutout
获取到"凹槽"的位置和尺寸(用Rect
表示),并将这些信息传递给adjustLayoutForCutout
方法。 - adjustLayoutForCutout
方法会遍历
DecorView及其所有子
View`,并根据"凹槽"信息调整它们的内边距,从而避免内容被"凹槽"遮挡。
通过这种方式,我们可以确保应用的界面能够完美适配异形全面屏,不会出现内容被"凹槽"遮挡的问题。
(2)、使用 Window Insets 适配异形全面屏的示例
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取 Window 的 DecorView
final View decorView = getWindow().getDecorView();
// 设置 OnApplyWindowInsets 监听器
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
// 获取系统 UI 遮挡的区域
Rect systemWindowInsets = new Rect(
insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
insets.getSystemWindowInsetBottom()
);
// 获取"安全区域"(即不被遮挡的区域)
Rect displayCutoutSafeInsets = insets.getDisplayCutout() != null
? insets.getDisplayCutout().getSafeInsets()
: new Rect();
// 打印调试信息
Log.d(TAG, "System UI insets: " + systemWindowInsets);
Log.d(TAG, "Display cutout safe insets: " + displayCutoutSafeInsets);
// 根据 insets 信息调整界面布局
adjustLayoutForInsets(decorView, systemWindowInsets, displayCutoutSafeInsets);
// 返回 insets 信息,以便系统继续处理
return insets;
}
});
}
private void adjustLayoutForInsets(View view, Rect systemWindowInsets, Rect displayCutoutSafeInsets) {
// 根据系统 UI 遮挡区域调整 view 的内边距
view.setPadding(
systemWindowInsets.left + displayCutoutSafeInsets.left,
systemWindowInsets.top + displayCutoutSafeInsets.top,
systemWindowInsets.right + displayCutoutSafeInsets.right,
systemWindowInsets.bottom + displayCutoutSafeInsets.bottom
);
// 如果 view 是 ViewGroup,则递归调整子 view
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
adjustLayoutForInsets(viewGroup.getChildAt(i), systemWindowInsets, displayCutoutSafeInsets);
}
}
}
}
在这个示例中:
-
我们为
Window
的DecorView
设置了OnApplyWindowInsetsListener
监听器,在这个监听器中获取系统 UI 遮挡的区域以及"安全区域"(即不被遮挡的区域)。 -
我们通过
WindowInsets
对象的getSystemWindowInsetXXX()
方法获取系统 UI 遮挡的区域,并通过getDisplayCutout().getSafeInsets()
方法获取"安全区域"。 -
我们将这两个区域信息传递给
adjustLayoutForInsets
方法,该方法会遍历DecorView
及其所有子View
,并根据遮挡区域信息调整它们的内边距,从而避免内容被遮挡。
通过这种方式,我们可以确保应用的界面能够完美适配异形全面屏,不会出现内容被系统 UI 遮挡的问题。
(3)、notch-friendly 导航栏适配异形全面屏的示例
Android 9.0 (Pie) 引入了"notch-friendly"的导航栏,可以自动适应异形全面屏。开发者可以通过设置 WindowManager.LayoutParams
来让应用的导航栏自动适配屏幕"凹槽"(Notch)。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取 Window 的 LayoutParams
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
// 设置 layoutParams 为 FLAG_LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
// 这样可以让导航栏自动适应屏幕"凹槽"
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
}
在这个示例中,我们主要做了以下几个步骤:
- 获取当前
Window
的LayoutParams
- 将
layoutInDisplayCutoutMode
属性设置为LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
。这个值表示应用的导航栏会自动适应屏幕"凹槽",避免内容被遮挡。 - 将修改后的
LayoutParams
应用到Window
上。
通过这种方式,我们可以确保应用的导航栏能够自动适应异形全面屏,不会出现内容被"凹槽"遮挡的问题。
值得注意的是,除了设置 layoutInDisplayCutoutMode
属性,Android 9.0 还提供了其他几种适配模式,开发者可以根据自己的需求进行选择:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
: 默认模式,系统会自动适配"凹槽"。LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
: 永远不会让内容进入"凹槽"区域。LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
: 适配"凹槽",但只会在屏幕短边(顶部或底部) 出现"凹槽"时生效。LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
: 总是让内容进入"凹槽"区域。
根据具体需求选择合适的模式,可以让应用的界面更好地适配异形全面屏。
三、Android 10 支持可折叠设备多窗口模式
Android 10 及以上版本引入了对可折叠设备的多窗口模式支持。开发者可以利用这些特性来为用户提供更好的体验。
下面代码实现可折叠设备多窗口模式适配的示例:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取 Window 的 DecorView
final View decorView = getWindow().getDecorView();
// 设置 OnApplyWindowInsets 监听器
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
// 获取窗口模式信息
int windowingMode = insets.getWindowingMode();
// 根据窗口模式调整界面布局
adjustLayoutForWindowingMode(decorView, windowingMode);
// 返回 insets 信息,以便系统继续处理
return insets;
}
});
}
private void adjustLayoutForWindowingMode(View view, int windowingMode) {
switch (windowingMode) {
case WINDOWING_MODE_MULTI_WINDOW:
// 多窗口模式下的布局调整
handleMultiWindowMode(view);
break;
case WINDOWING_MODE_FREEFORM:
// 自由模式下的布局调整
handleFreeformMode(view);
break;
default:
// 全屏模式下的布局调整
handleFullscreenMode(view);
break;
}
// 如果 view 是 ViewGroup,则递归调整子 view
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
adjustLayoutForWindowingMode(viewGroup.getChildAt(i), windowingMode);
}
}
}
private void handleMultiWindowMode(View view) {
// 多窗口模式下的布局调整逻辑,例如:
// - 调整字体大小
// - 调整控件尺寸
// - 调整控件间距
// - ...
}
private void handleFreeformMode(View view) {
// 自由模式下的布局调整逻辑,例如:
// - 调整字体大小
// - 调整控件尺寸
// - 调整控件间距
// - ...
}
private void handleFullscreenMode(View view) {
// 全屏模式下的布局调整逻辑,例如:
// - 恢复字体大小
// - 恢复控件尺寸
// - 恢复控件间距
// - ...
}
}
在这个示例中:
- 我们为
Window
的DecorView
设置了OnApplyWindowInsetsListener
监听器,在这个监听器中获取当前的窗口模式。 - 我们通过
WindowInsets
对象的getWindowingMode()
方法获取当前的窗口模式,并将其传递给adjustLayoutForWindowingMode
方法。 adjustLayoutForWindowingMode
方法会根据当前的窗口模式(多窗口、自由模式或全屏模式)调用相应的处理方法(handleMultiWindowMode
、handleFreeformMode
或handleFullscreenMode
)。- 在这些处理方法中,我们可以根据不同的窗口模式调整界面布局,例如调整字体大小、控件尺寸、控件间距等。
通过这种方式,我们可以确保应用的界面能够在不同的窗口模式下都能提供良好的用户体验。
需要注意的是,不同的可折叠设备可能会有不同的窗口模式支持,开发者需要根据具体的设备和场景进行适配和调整。同时,可以利用 Android Studio 的设备模拟器来进行测试和调试。
四、屏幕适配的最佳实践
除了上述重点内容,我们还需要注意以下一些屏幕适配的最佳实践:
-
尽量使用约束布局ConstraintLayout,减少嵌套层级
-
适度使用硬编码数值,避免滥用资源文件
-
谨慎使用绝对坐标,注意横竖屏切换情况
-
减少对wrap_content的过度使用
-
使用tools命名空间标签模拟数据,方便预览调整
-
持续关注Android新特性,跟上屏幕创新步伐
结语:
随着可折叠手机、环形屏等新形态的出现,Android屏幕适配面临的挑战也将与日俱增。作为开发者,我们需要不断学习创新,跟上屏幕革新的步伐,才能为用户呈现无与伦比的体验。对于Android未来的屏幕支持,你有什么期待?让我们拭目以待,共同努力打造最佳的屏幕适配实践!