大屏适配技术指南更新时间: 2025-04-18 14:45:00

1.屏幕兼容性

1.1 应用resizeable能力支持

Android 设备的形状和尺⼨多种多样,因此应⽤的布局需要⼗分灵活。也就是说,布局应该从容应对不同的屏幕尺⼨和⽅向,⽽不是为布局定义刚性尺⼨,假定屏幕尺⼨和宽⾼⽐是⼀定的。

所以多形态屏幕切换适配的本质是: 当应⽤运⾏时,屏幕的尺⼨、密度或⽐例发⽣了变化,应⽤能够继续在变化后的屏幕上正常显示和正常运⾏。

本节概述了这些主题以及 Android 上已有的可⽤功能,以帮助您的应⽤进⾏相应调整,⽀持不同屏幕尺⼨。

确保您的应⽤界⾯在不同的屏幕尺⼨下可以全屏的显示, 强烈推荐应用配置resizeableActivity为true以支持全屏显示

要适配多形态屏幕切换,⾸先是要让应⽤⽀持动态改变尺⼨,我们需要在 manifest 中的 Application 或对应的 Activity 下声明

android:resizeableActivity="true"

您需要根据应⽤⾯向的API Level (targetSdkVersion)进⾏⽀持resizeable能⼒的声明。

  • 如果应⽤程序⾯向API Level 24以上(targetSdkVersion>=24),系统将默认应⽤⽀持resizeable能⼒。
  • 如果应⽤程序⾯向API Level 24以下(targetSdkVersion < 24),需要应⽤在manifest中显式的声明android:resizeableActivity=true, 才可以⽀持resizeable能⼒ 。

备注:虽然安卓提供了申请受限屏幕能力,但强烈建议您为应用设计resizeable能力,因为一旦您声明了受限屏幕比例(最大或最小)这意味着,当您的应用程序运行在一个屏幕比例超出了您声明的范围,您的应用程序在屏幕上将出现黑边等现象,严重影响应用程序的用户体验。受限屏幕支持开发者适配指导如下 关于声明受限屏幕的使用说明

1.2 应用布局优化

在对不同尺⼨屏幕适配过程中,为了确保在多形态屏幕下获取最佳的布局显示效果,例如显示更多更清晰的内容,建议您对布局进⾏优化。

应⽤界⾯正确、美观的布局和显示,包含如下:

● 确保您的布局能够根据屏幕适当地调整⼤⼩;

● 根据屏幕配置提供合适的 UI 布局;

● 确保对正确的屏幕应⽤正确的布局;

● 提供可正常缩放的位图。

详细信息请参阅《大屏应用UX设计指南》,也可辅助参考Android开发者指南中  支持不同的屏幕尺寸 、  将界面迁移到自适应布局

2.应用连续性

为了保证您的应⽤程序在屏幕切换过程中⽆缝切换,您需要做应⽤连续性的设计,以确保您的应⽤程序任务不中断。最佳的体验为:应⽤在展开切换过程中,不发⽣应⽤的重启,且切换之前的任务和应⽤相关状态得以保存和延续。

三方应用支持连续性,需要在 AndroidManifest.xml 文件的 application 或者 actvivity 标签中添加 resizeableActivity=true 的属性:

 <application
android:resizeableActivity="true">
<activity
android:resizeableActivity="true" />
</application>

若应用的 targetSDK 为 24 或以上,即便不设置 resizeableActivity 属性,其默认也为 true。在设备发生屏幕切换后,应用应能妥善地保存界面状态或者支持配置变更。多形态屏幕切换的动作(折叠屏或者投屏),会触发对smallestscreensize、screensize和creenlayout以及方向和密度的配置更改。每当发⽣配置更改时,默认情况下会销毁并重新创建整个activity。推荐您通过注册监听系统configchanges消息,不重启应⽤的情况下处理配置更改,您需要向manifest中添加android:configchanges属性,其中⾄少包含以下值:

android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|density" 

其中,orientation在一些折叠屏展开默认横屏握持的设备上尤其重要,因为厂商会强制部分应用横屏显示;density在投屏场景应用较多,投屏场景无法保证不同屏幕的density是一致的。您需要复写 onConfigurationChanged() ⽅法,通过该⽅法的Configuration参数获得屏幕的分辨率、密度等信息,就可以针对不同⽐例屏幕下的应⽤界⾯布局做相应调整,如切换布局、调整控件位置和间距等。

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}

如果您的应⽤必须进⾏重新⾛⽣命周期来响应屏幕切换,需要进⾏状态的保存和恢复。

您可以通过onSaveInstanceState()和ViewModel对象来进⾏之前状态保存和后续的恢复。即,在销毁activity之前,通过onSaveInstancesState()存储状态, 在onCreate() or onRestoreInstanceState()进⾏状态的恢复。

Note:应用不要在OnDestroy()中调⽤finish()或其他方式⾃⾏终⽌进程。这将导致应⽤程序在设备多形态屏幕切换时关闭、闪退等问题。

应用不要自行hook资源上下文的config,而应由系统统一创建config和Resources对象。

详细信息请参阅Android开发者指南中  处理配置变更 、  保存界面状态

2.1 正确使用应用资源

1、无论是多窗口场景还是全屏场景,都强烈建议应⽤布局View要以窗⼝⼤⼩进⾏(因为多窗口场景窗口大小和屏幕大小并不等价),不可以按照屏幕⼤⼩布局,比如在窗⼝模式下还是以屏幕的宽⾼进⾏布局,则会导致应⽤的图标截断,布局错乱等布局问题。

2、推荐使用Activity上下文去获取窗口大小而不是获取屏幕大小,因为Activity Embedding、分屏、悬浮窗等多窗口场景屏幕大小与窗口大小不相等。

Android R开始:

获取当前 activity 窗口大小:

android.view.WindowMetrics windowMetrics = activity.getWindowManager().getCurrentWindowMetrics();
Rect windowRect = windowMetrics.getBounds();

获取当前activity所在的屏幕大小:

android.view.WindowMetrics maxWindowMetrics = this.getWindowManager().getMaximumWindowMetrics();
Rect maxWindowRect = maxWindowMetrics.getBounds();

Android R之前版本:

androidx.window.layout.WindowMetricsCalculator wtc =       
androidx.window.layout.WindowMetricsCalculator.getOrCreate();

androidx.window.layout.WindowMetrics windowMetrics =
wtc.computeCurrentWindowMetrics(activity);

Rect windowRect = windowMetrics.getBounds();

androidx.window.layout.WindowMetrics maxWindowMetrics =
wtc.computeMaximumWindowMetrics(activity);

Rect maxWindowRect = maxWindowMetrics.getBounds();

Android R之前版本需要在应用或模块的 build.gradle 文件中添加所需工件的依赖项:

dependencies {
implementation "androidx.window:window:1.1.0-alpha02"
// For Java-friendly APIs to register and unregister callbacks
implementation "androidx.window:window-java:1.1.0-alpha02"
// For RxJava2 integration
implementation "androidx.window:window-rxjava2:1.1.0-alpha02"
// For RxJava3 integration
implementation "androidx.window:window-rxjava3:1.1.0-alpha02"
// For testing
implementation "androidx.window:window-testing:1.1.0-alpha02"
}

详细信息请参阅Android开发者指南中  WindowManager JetPack

3、推荐使用Activity上下文去获取资源Configuration。

Configuration config = activity.getResources().getConfiguration();


应用不要hook获取资源activity.getResources()的接口,即应用不要自己构造Configuration和Resources去主动控制Configuration的分发和刷新。

4、正确获取控件View的位置

在多窗口情况下调用View的getLocationOnScreen和getLocationInWindow接口含义不同,前者拿到的控件在屏幕上的绝对位置(相对屏幕左上角起点),后者是控件在窗口内部的相对位置(相对于窗口左上角起点)。

5、使用合适的位图

在大屏设备上,图片如果被放大可能会显示不清晰,为此在需要放大的场景下,建议提供更高密度级别的图片并放到mipmap目录下,或者改矢量图形。在部分显示区域不能放大的场景使用.9图片和矢量图。

2.2 动态支持横竖屏切换

应用根据设备分辨率高宽比动态设置仅支持竖屏(或者横屏)和支持横竖屏切换,应用最佳体验是在小屏手机形态支持单一方向,比如竖屏或者横屏,大屏高宽接近的设备支持旋转屏(游戏、相机等特殊类型应用例外),如果应用设计与此目标相符,可以使用以下样例代码动态变更方向。

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRequestedOrientation(this, this.getResources().getConfiguration());
setContentView(R.layout.activity_main);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setRequestedOrientation(this, newConfig);
// update layout and redraw
}
public static void setRequestedOrientation(Activity activity, Configuration config) {
float longSide = Math.max(config.screenWidthDp, config.screenHeightDp);
float shortSide = Math.min(config.screenWidthDp, config.screenHeightDp);
boolean isHeighRatio = longSide / shortSide > 16f / 9;
if (isHeighRatio) {
// phone, foldable device in folded mode, pocket phone in expanded mode
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
// tablet, foldable device in expanded mode, pocket phone in folded mode, tv
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}
}


详细请参考Google官方文档:  应用连续性

3.多屏多窗口

3.1 支持分屏和悬浮窗模式

应⽤需要⽀持resizeable,以允许系统根据⽤户场景需要触发按分屏或悬浮窗模式启动应⽤。

详细信息请参阅Android开发者指南中  多窗口支持

3.2 多项恢复

在搭载 Android 9.0 及更低版本的设备上运⾏时,只有获得焦点的应⽤处于已恢复状态(Resumed)。任何其他可⻅ Activity 都处于已暂停状态(Paused)。如果应⽤在处于暂停状态时关闭资源或停⽌播放内容,则可能会产⽣问题。

从 Android 10 开始,此⾏为发⽣了变化,即当设备处于多窗⼝模式时,所有 Activity 都会保持已恢复状态。这称为多项恢复(Multi-resume)。请注意,如果顶部有透明 Activity,或者Activity 不可成为焦点(例如,处于画中画模式),则相应 Activity 可能会处于已暂停状态。还有⼀种可能是,在特定时间内(例如,当打开抽屉式通知栏时)所有 Activity都不具有焦点。onStop会继续照常⼯作。每当 Activity 从屏幕上移除时,系统都会调⽤它。

部分搭载 Android 9.0 的设备也提供多项恢复功能。如要在这些设备上选择启⽤多项恢复功能,您可以添加以下清单元数据:

<meta-data android:name="android.allow_multiple_resumed_activities" android:value="true" />

3.3 专属资源访问

为了帮助⽀持多项恢复功能,Android提供了⼀个新的⽣命周期回调Activity#onTopResumedActivityChanged()。

当 Activity 获得或失去顶部恢复 Activity 位置时(即处于Multi-resume的多个Resumed状态的activity在获取焦点或者丢失焦点时),系统会调⽤此⽅法。当 Activity 使⽤共享的单⽤户资源(例如⻨克⻛或摄像头)时,了解这⼀点⾄关重要。

protected void onTopResumedActivityChanged(boolean topResumed) {
if (topResumed) {

} else {

}
}

请注意,应⽤可能会因其他多种原因丢失资源,例如移除共享硬件。

在任何情况下,应⽤都应妥善处理会影响可⽤资源的资源丢失事件和状态更改。

对于使⽤摄像头的应⽤,建议使⽤

CameraManager.AvailabilityCallback#onCameraAccessPrioritiesChanged() ⽅法作为提示,提醒这可能是尝试访问摄像头的好时机。此⽅法在Android 10 (API 级别 29) 或更⾼版本 4.3.3 中可⽤。

当收到Activity#onTopResumedActivityChanged(topResumed)回调时,

● topResumed = false时,需要在此时判断是否释放独占资源,而不必在一失去焦点时就释放资源;

● topResumed = true时 ,可以申请独占的摄像头资源,原持有摄像头资源的应用将收到 CameraDevice.StateCallback#onDisconnected() 回调后,对摄像头设备进行的后续调用将抛出 CameraAccessException

请记住,resizeableActivity=false 并不保证可以获取对摄像头的专属访问权限,因为其他使⽤摄像头的应⽤可能会在其他显示屏上打开。

3.4 多窗口下请求方向

在多窗⼝模式下,运⾏时锁定朝向的⽅法都是⽆效的,也就是说多窗口场景下,仅支持竖屏的应用也有可能存在窗口宽比高大这种横屏的场景,所以界面布局一定要同时考虑宽比高小和宽比高大两类场景。

1)setRequestedOrientation()

2)android:screenOrientation

3.5 多显示屏

Android 10 (API 级别 29) 或更⾼版本⽀持辅助显示屏上的 Activity。如果 Activity 在具有多个显示屏的设备上运⾏,则⽤户可以将 Activity 从⼀个显示屏移到另⼀个显示屏。多项恢复功能也适⽤于多屏场景。多个 Activity 可以同时接收⽤户输⼊。

应⽤可以指定在启动或创建其他 Activity 时它应该在哪个显示屏上运⾏。该⾏为取决于清单⽂件以及 Intent 标记和选项(由启动 Activity 的实体设置)中定义的 Activity 启动模式。

与多形态屏幕切换⼀样,当 Activity 移⾄辅助显示屏时,系统会更新上下⽂、调整窗⼝⼤⼩,并更改配置和资源。如果由该 Activity 来处理配置变更,则其会在 onConfigurationChanged() 中收到通知。如果不是,则其会重新启动。

如果配置变更已处理,则 Activity 应该在 onCreate 和 onConfigurationChanged 中检查当前显示屏。确保在显示屏变更时更新资源和布局。

如果为 Activity 选择的启动模式⽀持多个实例,则请记住,在辅助屏幕上启动时会新建⼀个 Activity 实例。这两个 Activity 会同时恢复。


更多多显示屏相关内容请参阅Android开发者指南中  多显示屏

4.折叠屏适配

4.1 折叠屏分类

目前纵向折叠屏,与普通直屏手机,仅有屏幕可折叠的差异,此处不做讨论。

在横向折叠屏中,有两种形态存在,展开为默认为横屏的折叠屏设备和展开默认为竖屏的折叠屏设备。

4.2 两类折叠屏差异

展开默认是竖屏的折叠屏设备,展开大屏竖屏的rotation是0度、屏幕方向是portrait、宽小于高,与普通手机认知一致。

对于展开默认是横屏的设备,在展开大屏横屏时rotation是0度,屏幕方向是landscape,宽大于高。

4.3 折叠屏适配建议

1、推荐应用根据设备分辨率高宽比动态设置仅支持竖屏和支持横竖屏切换。

2、响应onConfigurationChanged保证业务连续性。

3、应用做布局时不要使用rotation,而是根据宽高的大小做布局处理,避免两类折叠屏重复适配布局问题。

4、应用布局同时考虑横竖屏方向,比如SurfaceView实现的直播和短视频页面,以及Webview加载H5实现的页面比如运营页、小程序/小游戏等尤其需要同时考虑宽高两个方向尺寸进行布局,以应对第二类折叠屏展开显示场景。

5、对于一些视频最大化横屏播放场景,判断点击最大化的逻辑,建议不要与横竖屏状态关联,而是维护一个是否已经最大化的状态。

6、使用摄像头时要注意横竖屏场景以及前后置摄像头,及时在生命周期中刷新预览方向和图片大小,确保设备旋转能够正常显示预览和拍照。

7、对于宽高比小于16:9的窗口,避免根据屏幕宽度来缩放density或者重写measure,H5不设置viewport的scale,改用增加横向列表数量,或增加间距的方式铺满屏幕。

8、考虑折叠屏大屏宽大于高的情况,游戏的画面铺满整个屏幕,避免出现没有铺满屏幕导致的花屏现象。

4.4 折叠屏状态监听和获取

1、折叠屏折叠和展开仅仅是display的尺寸发生变化,并通过onConfigurationChanged通知应用去更新布局,应用在onConfigurationChanged中获取自身的WindowMetrics去动态自适应布局,应用不应关心折叠和展开的状态。

2、小米从Android T版本开始,均支持通过Google的JetPack去感知展开态和半折态和半折态区域,用于应用监听半折态做悬停布局,目前提供的展开态和半折态分别是FLAT和HALF_OPENED,提供展开态和半折态的区分是因为这两种状态都处于同一display尺寸下,并不会有onConfigurationChanged上报给应用,所以只能新增监听让应用可以辨识出展开态和半折态,在展开和半折的状态切换回调中做布局更新,达到半折态悬停效果。

折叠屏半折有两种情况:

1)上下对折,下半屏放置桌面上使用,即TableTop桌面模式

2)左右对折,像翻书一样使用,即Book书本模式

注意:如果应用支持上面两种状态的切换,切换时状态会重新通知,即若应用页面支持旋转屏,则转屏时对折方向会发生变化,系统会重新通知监听者,应用可以在收到监听时重新获取对折方向进行布局更新。

3、Jetpack WindowManager 是为了支持不同的可折叠设备外形规格而构建的,其设计也旨在支持未来发布的设备。为了支持未来的兼容性,系统会在 DisplayFeature(折叠屏实际类型是FoldingFeature,继承自DisplayFeature)元素的列表中将显示屏信息作为 WindowLayoutInfo 的一部分提供。此基本接口描述了显示屏的物理功能。

FoldingFeature关键API如下:

● getState() :获取展开态还是半折态,取值有FoldingFeature.State.FLAT和FoldingFeature.State.HALF_OPENED,折叠态比较特殊,目前没有折叠态的取值,折叠态会直接走onConfigurationChanged(折叠态和展开态的切换会涉及到display大小的切换,会触发应用走onConfigurationChanged;半折态和展开态由于display的大小都没变化,所以不会触发应用走onConfigurationChanged),应用如果非要获取折叠态,可以在LayoutStateChangeCallback的回调accept中根据WindowLayoutInfo获取到getDisplayFeatures()的size是0表明已经没有展开态和半折态了,即已经从展开态或者半折态切换到折叠态。

半折态时,若应用页面支持横竖屏,则LayoutStateChangeCallback会重新回调,开发者可以获取到新的折痕位置和对折方向,以便刷新半折态时的视图。

● getBounds():获取折叠屏折痕位置,上下对折时返回的区域宽大于高,比如[0, 960, 1792, 960];左右对折时返回的区域宽小于高,比如[960, 0, 960, 1792]。

● getOrientation():折叠屏对折方向,FoldingFeature.Orientation.HORIZONTAL表明是上下对折、FoldingFeature.Orientation.VERTICAL表明是左右对折。

开发时需要在应用或模块的 build.gradle 文件中添加所需工件的依赖项:

dependencies {
implementation "androidx.window:window:1.1.0-alpha02"
// For Java-friendly APIs to register and unregister callbacks
implementation "androidx.window:window-java:1.1.0-alpha02"
}

具体样例代码:

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback = new LayoutStateChangeCallback();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
windowInfoTracker = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart(); {
windowInfoTracker.addWindowLayoutInfoListener(this, Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
// Use newLayoutInfo to update the Layout
List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
if (displayFeatures.size() == 0) {
// enter folded state, you can update the layout here or in onConfigurationChanged()
}
for (DisplayFeature feature : displayFeatures) {
if (feature instanceof FoldingFeature) {
if (isTableTopPosture((FoldingFeature) feature)) {
enterTabletopMode(feature);
} else if (isBookPosture((FoldingFeature) feature)) {
enterBookMode(feature);
} else if (isSeparating((FoldingFeature) feature)) {
// Dual-screen device
if (((FoldingFeature) feature).getOrientation() ==
FoldingFeature.Orientation.HORIZONTAL) {
enterTabletopMode(feature);
} else {
enterBookMode(feature);
}
} else {
enterNormalMode();
}
}
}
}
}
private boolean isTableTopPosture(FoldingFeature foldFeature) {
return (foldFeature != null)
&& (foldFeature.getState() == FoldingFeature.State.HALF_OPENED)
&& (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}
private boolean isBookPosture(FoldingFeature foldFeature) {
return (foldFeature != null)
&& (foldFeature.getState() == FoldingFeature.State.HALF_OPENED)
&& (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}
private boolean isSeparating(FoldingFeature foldFeature) {
return (foldFeature != null)
&& (foldFeature.getState() == FoldingFeature.State.FLAT)
&& (foldFeature.isSeparating() == true);
}

详细信息请参阅Android开发者指南中 让应用具备折叠感知能力

5.WebView页面屏幕适配

基于响应式Web设计来实现界面的显示和布局是目前主流的H5页面设计方式,但也存在问题。由于没有对折叠屏的窗口变化进行合理分析,可能会在展开态存在如下界面显示问题:

● 界面元素等比放大,显示效果差:如字体太大、图片太大/模糊等。

● 界面元素延展到界面外,无法操作:如广告框的关闭按钮显示在界面外,无法关闭。

● 界面内容重叠、错位:如输入框中文字部分显示被截断。

为了避免出现如上基础体验问题,需要对问题界面进行调整。

适配可参考文档 《Webview适配指导》

6.基础信息获取

6.1获取屏幕显示器

/**
* Get the display associated with this context.
* NOTE: context like {@link android.app.Application} and {@link android.app.Service}
* is not associated with display. In this case {@link Display#DEFAULT_DISPLAY} will
* be returned, although it is not a recommended practice.
*/
public static Display getDisplay(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
return context.getDisplay();
} catch (UnsupportedOperationException e) {
Log.w(TAG, "This context is not associated with a display. " +
"You should use createDisplayContext() to create a display " +
"context to work with windows.");
}
}
return getWindowManager(context).getDefaultDisplay();
}

6.2获取窗口信息

获取可见区域的窗口大小,包括全屏时的状态栏和导航栏。

/**
* Get the window size of all visible area associated with given context, including
* status bar and navigation bar if fullscreen. This differs from
* {@link #getScreenSize(Context, Point)} as it will compute the area this
* context is able to use currently. <br>
* <b>NOTE: It is not recommended to use this method with a context like
* {@link android.app.Application} or {@link android.app.Service}, or the result may
* be confused.</b>
*
* @param context better be a window-associated context.
* @param windowSize the result will be stored in.
* @see #getScreenSize(Context, Point)
*/
public static void getWindowSize(Context context, Point windowSize) {
getWindowSize(getWindowManager(context), context, windowSize);
}
public static Point getWindowSize(Context context) {
Point point = new Point();
getWindowSize(context, point);
return point;
}
public static Point getWindowSizeDp(Context context) {
float density = context.getResources().getDisplayMetrics().density;
Point windowSize = getWindowSize(context);
windowSize.x = (int) (windowSize.x / density);
windowSize.y = (int) (windowSize.y / density);
return windowSize;
}
public static void getWindowSize(WindowManager wm, Context context, Point point) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// above Android S, we assume that an Activity or WindowContext is used.
Rect bounds = wm.getCurrentWindowMetrics().getBounds();
point.x = bounds.width();
point.y = bounds.height();
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// If window-unassociated context is passed, getCurrentWindowMetrics() will return
// the most recent window size, which may be confusing.
Context baseContext = context;
while (baseContext instanceof ContextWrapper) {
if (baseContext instanceof Activity) {
break;
}
baseContext = ((ContextWrapper) baseContext).getBaseContext();
}
Rect bounds;
if (baseContext instanceof Activity) {
bounds = wm.getCurrentWindowMetrics().getBounds();
} else {
// not Activity, so maximum metrics should be a good choice.
bounds = wm.getMaximumWindowMetrics().getBounds();
}
point.x = bounds.width();
point.y = bounds.height();
} else if (isInMultiWindowMode(context)) {
getDisplay(context).getSize(point);
} else {
getDisplay(context).getRealSize(point);
}
}
public static boolean isInMultiWindowMode(Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return checkMultiWindow((Activity) context);
}
context = ((ContextWrapper) context).getBaseContext();
}
return false;
}
private static boolean checkMultiWindow(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return activity.isInMultiWindowMode();
}
return false;
}

6.3获取屏幕尺寸

public static Point getScreenSize(Context context) {
Point point = new Point();
getScreenSize(context, point);
return point;
}
public static void getScreenSize(Context context, Point point) {
getScreenSize(getWindowManager(context), context, point);
}
public static void getScreenSize(WindowManager wm, Context context, Point point) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Rect bounds = wm.getMaximumWindowMetrics().getBounds();
point.x = bounds.width();
point.y = bounds.height();
} else {
getDisplay(context).getRealSize(point);
}
}

6.4小窗模式判断

小窗模式判断

/**
* The device enters the free window mode
*/
public static boolean isFreeformMode(Configuration config, Point screenSize, Point windowSize) {
float factorW = (windowSize.x + 0f) / screenSize.x;
float factorH = (windowSize.y + 0f) / screenSize.y;
String configuration = config.toString();
return configuration.contains("mWindowingMode=freeform") && (factorW <= 0.9f || factorH <= 0.9f);
}

6.5横竖屏状态判断

/**
* @param context
* @return
*/
public static boolean isPortrait(@NonNull Context context) {
// Above Android S, we assume that an Activity or WindowContext is used.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || isActivity(context)) {
return context.getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT;
}
// This is not an Activity, so there is no actual orientation.
// We use application orientation to decide it, which should be the display
// orientation.
return context.getApplicationContext().getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT;
}

6.6断点定义

断点要求:可服务于长期更多新设备

断点实现

public static int getWindowSizeType(Context context) {
float density = context.getResources().getDisplayMetrics().density;
Point windowSize = getWindowSize(context);
int windowWidth = (int) (windowSize.x / density);
(windowWidth <= 600) {
return TYPE_WINDOW_SIZE_SMALL;
} else if (windowWidth <= 840) {
return TYPE_WINDOW_SIZE_MEDIUM;
} else {
return TYPE_WINDOW_SIZE_LARGE;
}
}

7.分栏场景适配

大屏设备使用户能看到更多内容、执行更多操作、体验更多功能,通常会采用分栏布局,适配宽屏提高交互效率。常见的场景例如详情、评论、个人信息等。分栏常见的几种实现方式下面会详细介绍。

7.1 SlidingPaneLayout

SlidingPaneLayout是Androidx库中提供的一个布局控件,主要用于实现水平方向的双窗格布局。该布局允许用户通过滑动来显示或隐藏侧边面板,非常适合在大屏幕设备(如平板电脑)上使用,以提供更为丰富和直观的用户界面。

功能特点

  • 水平双窗格布局:SlidingPaneLayout提供了一个水平的双窗格布局,其中一个窗格(通常是左侧窗格)作为导航或列表视图,另一个窗格(通常是右侧窗格)作为内容视图。
  • 自适应屏幕尺寸:根据设备的屏幕宽度,SlidingPaneLayout可以自动调整两个窗格的布局方式。在屏幕宽度足够时,两个窗格可以并排显示;在屏幕宽度不足时,侧边窗格可以滑动隐藏,只显示内容窗格。
  • 滑动交互:用户可以通过手指滑动来显示或隐藏侧边窗格,提供流畅的交互体验。

应用场景

  • 列表/详细信息界面:在新闻阅读、邮件应用等场景中,可以使用SlidingPaneLayout来显示列表和详细信息。
  • 导航菜单:在应用中实现侧边导航菜单,用户可以通过滑动来访问不同的功能或页面。
  • 多媒体应用:在视频、音乐等多媒体应用中,可以使用SlidingPaneLayout来显示播放列表和播放控制界面。

使用介绍

1)在布局文件中使用

<androidx.slidingpanelayout.widget.SlidingPaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 左侧窗格(主视图) -->
<FrameLayout
android:id="@+id/left_pane"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/white">
<!-- 左侧内容 -->
</FrameLayout>
<!-- 右侧窗格(详情视图) -->
<FrameLayout
android:id="@+id/right_pane"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray">
<!-- 右侧内容 -->
</FrameLayout>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

2)在代码中控制滑动

SlidingPaneLayout 提供了一些方法,可以在代码中控制滑动行为:

  • openPane():打开右侧窗格。
  • closePane():关闭右侧窗格,显示左侧窗格。
  • isOpen():检查右侧窗格是否完全打开。
  • isSlideable():检查是否可以滑动。

SlidingPaneLayout slidingPaneLayout = findViewById(R.id.sliding_pane_layout);
// 打开右侧窗格
slidingPaneLayout.openPane();
// 关闭右侧窗格
slidingPaneLayout.closePane();
// 检查右侧窗格是否打开
if (slidingPaneLayout.isOpen()) {
// 右侧窗格已打开
}

3)属性介绍

SlidingPaneLayout 支持一些自定义属性,可以通过 XML 或代码设置:

  • XML 属性
    • android:layout_weight:设置子视图的权重,用于分配剩余空间。
    • android:layout_gravity:设置子视图的对齐方式(如 start或 end)。
    • android:background:设置背景颜色或图片。

  • 代码属性
    • setSliderFadeColor(int color):设置滑动时边缘的渐变颜色。
    • setShadowDrawableLeft(Drawable d):设置左侧窗格的阴影。
    • setShadowDrawableRight(Drawable d):设置右侧窗格的阴影。
    • setParallaxDistance(int parallaxBy):设置滑动时的视差效果距离。

4)事件监听

slidingPaneLayout.setPanelSlideListener(new SlidingPaneLayout.PanelSlideListener() {
@Override
public void onPanelSlide(View panel, float slideOffset) {
// 滑动过程中调用
// slideOffset:滑动偏移量(0 表示关闭,1 表示完全打开)
}
@Override
public void onPanelOpened(View panel) {
// 右侧窗格完全打开时调用
}
@Override
public void onPanelClosed(View panel) {
// 右侧窗格完全关闭时调用
}
});


开发者资源

7.2 使用Fragment

Fragment 表示应用界面中可重复使用的一部分。fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。fragment 不能独立存在。它们必须由 activity 或其他 fragment 托管。fragment 的视图层次结构会成为宿主的视图层次结构的一部分或附加到宿主的视图层次结构。

fragment 允许您将界面划分为离散的区块,从而将模块化和可重用性引入 activity 的界面。activity 是围绕应用的界面放置全局元素(如抽屉式导航栏)的理想位置。相反,Fragment 更适合定义和管理单个屏幕或部分屏幕的界面。

使用介绍

使用Fragment实现分栏布局,实现思路:

  • 宽屏(如平板横屏/竖屏/Fold内屏):
    • 使用双窗格布局,左侧显示列表(列表视图L),右侧显示详情(详情视图C)。
    • 通过 FragmentManager动态加载两个 Fragment。

  • 窄屏(如手机/Fold外屏/Pad分屏)
    • 使用单窗格布局,默认显示列表(列表视图L)。
    • 点击列表项后,替换为详情 Fragment。

  • 根据断点切换布局形态
    • 显示窗口由宽屏动态切换到窄屏,双窗格布局应切换为单窗格布局,此时应该默认显示详情视图C。

1)创建布局文件

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- 左侧窗格(列表) -->
<FrameLayout
android:id="@+id/list_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<!-- 右侧窗格(详情) -->
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
</LinearLayout>


2)创建Fragment

  • 实现列表Fragment(java)
  • 实现详情Fragment(java)
public class DetailFragment extends Fragment {
private static final String ARG_ITEM = "item";
public static DetailFragment newInstance(String item) {
DetailFragment fragment = new DetailFragment();
Bundle args = new Bundle();
args.putString(ARG_ITEM, item);
fragment.setArguments(args);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_detail, container, false);
// 显示详情内容
return view;
}
}


3)Activity中加载Fragment

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 加载列表 Fragment
ListFragment listFragment = new ListFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.list_container, listFragment)
.commit();
}
DetailFragment detailFragment = new DetailFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_container, detailFragment)
.commit();
}
}


4)布局动态响应调整

需要根据断点控制列表视图和详情视图的显示隐藏

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (getWindowSizeType(context) == TYPE_WINDOW_SIZE_LARGE ||
getWindowSizeType(context) == TYPE_WINDOW_SIZE_MEDIUM) {
//显示列表视图和详情视图
} else {
//隐藏列表视图,展示详情视图
}
}


5)Fragment返回栈控制

需要合理的处理Fragment的回退栈,避免不友好的用户体验出现。

  • 将事务添加到回退栈:在 FragmentTransaction中,通过 addToBackStack(String name) 方法可以将当前事务添加到回退栈中。
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, new MyFragment());
transaction.addToBackStack("fragment_tag"); // 添加到回退栈
transaction.commit();

  • 从回退栈中弹出事务:当用户按下返回键时,系统会自动从回退栈中弹出最近的事务,也可以通过代码手动弹出事务。

//方法 1:弹出栈顶事务
getSupportFragmentManager().popBackStack();
//方法 2:弹出到指定名称的事务
getSupportFragmentManager().popBackStack("fragment_tag", FragmentManager.POP_BACK_STACK_INCLUSIVE);
//方法 3:立即弹出事务
getSupportFragmentManager().popBackStackImmediate();

  • 回退栈的声明周期:当一个 Fragment被添加到回退栈中时,它的生命周期会发生变化。
    • 添加到回退栈时:Fragment会经历 onPause()、onStop() 和 onDestroyView(),但不会调用 onDestroy() 和 onDetach()。
    • 从回退栈中恢复时:Fragment会重新创建视图并调用 onCreateView()、onStart() 和 onResume()。

开发者资源

7.3 Activity 嵌入

显示屏提供了同时运行多个 activity 或同一 activity 的多个实例的机会。为了利用大屏幕的额外显示区域,Android 12L开始Jetpack WindowManager引入了Activity Embedding,该功能可以在 activity 之间拆分应用的任务窗口。

不同于前面系统层面,不同应用间的分屏,Activity Embedding只需要对应用进行很少的重构或根本不需要对应用进行重构。您通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定应用如何显示其 activity(并排或堆叠)。系统会自动维护对小屏幕的支持。当应用在配备小屏幕的设备上时,activity 会相互堆叠。在大屏幕上,activity 会并排显示。系统会根据您已创建的配置(不需要分支逻辑)来确定呈现方式。

Activity Embedding支持设备屏幕方向的变化,并且可以在可折叠设备上无缝工作,该功能会随着设备折叠和展开而堆叠和取消堆叠 activity。但如果应用由多个 activity 组成,activity 嵌入可让您轻松地在平板电脑、可折叠设备和 Chrome 操作系统设备上提供增强的用户体验。

适配可参考文档《Activity嵌入适配指导》

8.浮窗/弹窗适配

浮窗/弹窗是大屏设备上的模态面板,在大屏上用合适宽度的中型容器,承载小屏上的全屏页面,旨在提升大屏上的交互与视觉体验。浮窗/弹窗有离开主要进程、“临时态”的隐喻,用于非主线使用场景(如设置、新建、查看信息),应避免在主线重要场景使用浮窗。

8.1 WindowManager实现

WindowManager是Android系统中的一个核心服务,它负责管理应用程序窗口的布局、显示、位置和大小等,以及与其他窗口的交互。

适用场景

WindowManager常用于创建浮动窗口,这些窗口可以悬浮在其他应用或系统界面之上。例如,实现悬浮球、悬浮菜单、悬浮通知等功能。这种窗口通常用于提供快捷操作、展示重要信息或实现特定的交互效果。

使用介绍

  • WindowManager是一个接口,继承自 ViewManager。它提供了以下核心方法:
  • addView(View view, ViewGroup.LayoutParams params):将 View添加到屏幕上。
  • updateViewLayout(View view, ViewGroup.LayoutParams params):更新 View的布局参数。
  • removeView(View view):从屏幕上移除 View。
  • LayoutParams是一个类,用于定义窗口的布局参数,包括:
    • 窗口类型(type):例如 TYPE_APPLICATION_OVERLAY、TYPE_PHONE等。
    • 窗口标志(flags):例如 FLAG_NOT_FOCUSABLE、FLAG_NOT_TOUCHABLE等。
    • 窗口位置和大小:通过 x、y、width、height等属性设置。
    • 窗口层级(gravity):例如 TOP、Gravity.START等。

WindowManager实现全局弹窗的方式这里不做详细的赘述,开发者可以重点关注一下Window的弹窗如何根据窗口断点改变响应式调整布局样式:

WindowManager本身没法接收configuration的变化回调,这里建议通过WindowManager内inflate的自定义View来接收configuration变化的回调,基础的实现方式如下:

public class DialogRootView extends FrameLayout {
private ConfigurationChangedCallback mCallback;
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mNotifyConfigChanged = true;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mCallback != null) {
mCallback.onConfigurationChanged(getResources().getConfiguration(),
left, top, right, bottom);
}
}
public void setConfigurationChangedCallback(ConfigurationChangedCallback callback) {
mCallback = callback;
}
public interface ConfigurationChangedCallback {
void onConfigurationChanged(Configuration newConfig, int left, int top, int right, int bottom);
}
}


在onLayout回调内通知onConfigurationChanged,对于left、top、right、bottom的值获取会更加准确。

如果进入、退出分屏场景,出现rootView尺寸不准确的情况,可尝试post方式,延迟一帧再响应调整布局。

    @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
post(new Runnable() {
@Override
public void run() {
Configuration newConfig = getResources().getConfiguration();
Configuration updatedConfig;
updatedConfig = newConfig;
if (updatedConfig.screenWidthDp != wDp || updatedConfig.screenHeightDp != hDp) {
if (mCallback != null) {
mCallback.onConfigurationChanged(updatedConfig, left, top, right, bottom);
}
}
}
});
}


弹窗响应式调整

floatingView = LayoutInflater.from(this).inflate(R.layout.popup_layout, null);
floatingView?.setConfigurationChangedCallback(object : DialogRootView.ConfigurationChangedCallback {
override fun onConfigurationChanged(
newConfig: Configuration?,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
//进行响应式布局调整
}
})

8.2 自定义Dialog实现

自定义Dialog的使用场景广泛,主要出现在需要向用户展示特定信息、收集用户输入或引导用户进行选择性操作的场合。

适用场景

自定义Dialog的使用场景包括但不限于:

  • 确认提示:当用户执行某些关键操作时,如删除文件、退出登录等,弹出自定义弹窗询问用户是否确认执行该操作。
  • 收集用户输入:通过自定义Dialog中的输入框,收集用户输入的信息,如用户名、密码、评论等。
  • 提供详细信息:展示一些详细的帮助信息、设置选项或应用更新日志等,这些信息可能不适合直接展示在界面上,但用户需要时可以随时查看。
  • 实现特定功能:自定义Dialog还可以用于实现一些特定的功能,如列表选择、进度显示、图片预览等。

使用介绍

推荐应用方对Diaog进行二次封装,样式更灵活,更方便实现响应式布局。

下面是简单封装示例,业务可根据自身业务场景进行封装:

public class CustomDialog extends Dialog {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(view);
Window win = getWindow();
WindowManager.LayoutParams lp = win.getAttributes();
lp.gravity = Gravity.CENTER;
lp.height = height;
win.setAttributes(lp);
}
public static final class Builder {
public Builder(Context context) {
this.context = context;
}
public Builder setCustomView(int resView) {
view = LayoutInflater.from(context).inflate(resView, null);
return this;
}
public Builder setHeight(int heightValue) {
height = heightValue;
return this;
}
public CustomDialog build() {
return new CustomDialog(this);
}
}
}


设置监听回调

 view.setConfigurationChangedCallback(new DialogRootView.ConfigurationChangedCallback() {
@Override
public void onConfigurationChanged(Configuration newConfig, int left, int top, int right, int bottom) {
CustomDialog.this.onConfigurationChanged(newConfig);
}
});


config配置变更,响应式调整布局

 private void onConfigurationChanged(Configuration newConfig) {
//1.根据断点进行响应式布局调整
//2.更新布局
}


开发者资源

8.3 DialogFragment实现

DialogFragment本质上是一个Fragment,Android官方推荐的弹窗的实现方式,相对于Dialog它具有更高的可复用性(降低耦合)和更好的便利性(很好的处理屏幕形态变化的情况)。

适用场景

DialogFragment相对于Dialog能够支持更加复杂的UI和逻辑处理,并且能够更好的处理配置的更改,根据断点响应式处理布局。

使用介绍

实现时应继承此类,并重写 onCreateView() 方法以提供对话框的内容。或者也可以重写 onCreateDialog(android.os.Bundle) 方法来创建一个完全自定义的对话框,例如带有自己内容的 AlertDialog。

自定义DialogFragment

每一个继承了 Fragment 的类都必须有一个空参的构造方法,这样当 Activity 被恢复状态时 Fragment 能够被实例化。 Google强烈建议我们不要使用构造方法进行传参,因为 Fragment 被实例化的时候,这些带参构造函数不会被调用。如果要传递参数,可以使用 setArguments(bundle) 方式来传参。

public class CustomDialogFragment extends DialogFragment {
private String content;
static CustomDialogFragment newInstance(String content) {
CustomDialogFragment customDialogFragment = new CustomDialogFragment();
Bundle bundle = new Bundle();
bundle.putString("content", content);
customDialogFragment.setArguments(bundle);
return customDialogFragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getArguments();
if (bundle != null) {
content = bundle.getString("content");
}
}
}


使用DialogFragment创建弹窗

DialogFragment的使用也是非常简便,有2种方式来创建DialogFragment。

1)重写onCreateView()创建

onCreateView()方法内加载自定义布局,适用于使用自定义样式弹窗的情况。

@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//加载布局
View view = inflater.inflate(R.layout.dialog_coustom, container);
initView(view);
return view;
}

注意,上述自定义dialog_coustom.xml里面即使定义弹窗宽高为MATCH_PARENT,可见在布局文件中设置DialogFragment样式是无效的,

这也是使用DialogFragment需要注意的问题。下面将说明如何解决该问题。

2)重写onCreateDialog()

在该方法内使用AlertDialog等创建dialog,适用于使用系统样式弹窗的情况。

@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("标题");
builder.setMessage(content);
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//处理点击事件
}
});
return builder.create();
}


DialogFragment展示

创建好了弹窗,只需要创建DialogFragment对象,然后调用show方法就可以显示出来了

CustomDialogFragment dialog = CustomDialogFragment.newInstance();
dialog.show(supportFragmentManager, "dialog");


需要注意的是,如果同时重写这2种创建dialog的方法,那么在显示时以onCreateDialog为最终效果

DialogFragment调整布局

上面提到,如果使用自定义方式创建dialog,那么在布局文件中生命的样式是无用的。为了解决这个问题,需要在DialogFragment的onStart()回调中获取Dialog的Window对象,通过Window对象来设置Dialog的布局和样式。例如

@Override
public void onStart() {
super.onStart();
Window dialogWindow = getDialog().getWindow();
if (dialogWindow != null) {
dialogWindow.setLayout((int) (getResources().getDisplayMetrics().widthPixels * 0.9f),
ViewGroup.LayoutParams.WRAP_CONTENT);
WindowManager.LayoutParams params = dialogWindow.getAttributes();
params.gravity = Gravity.BOTTOM;
dialogWindow.setAttributes(params);
}
}


DialogFragment本身是Fragment,相较于Dialog可以更方便的处理configuration的变化来进行响应式的布局调整。

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Window dialogWindow = getDialog().getWindow();
WindowManager.LayoutParams lp = dialogWindow.getAttributes();
dialogWindow.setLayout((int) (getResources().getDisplayMetrics().widthPixels * 0.8f),
ViewGroup.LayoutParams.WRAP_CONTENT);
if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
lp.gravity = Gravity.BOTTOM;
dialogWindow.setAttributes(lp);
} else {
lp.gravity = Gravity.CENTER;
dialogWindow.setAttributes(lp);
}
}


开发者资源

8.4 透明背景Activity实现

如果想要弹窗的生命周期、交互方式、布局样式可以自定义,并且可以承载Native/Web Fragment的展示,建议采用透明背景的Activity实现。并且浮窗/弹窗会自动根据窗口尺寸来切换浮窗/全屏模式,例如在一般竖屏手机上就是全屏,在Fold内屏和Pad全屏时就是浮窗。

适用场景

1.承载WebView前端页面,通过浮窗显示合理布局

2.类似设置页这种,会有相同类似的页面结构,需要用浮窗来承载且可以点击跳转。

使用介绍

Activity实现半透明背景的浮窗需要注意适配状态栏和小白条实现沉浸式效果:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//设置小白条沉浸式效果
this.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
//设置状态栏沉浸式效果
this.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
}

小白条沉浸式适配效果需要特殊处理经典导航键模式,建议在全面屏手势模式设置导航栏沉浸态,在经典导航键模式移除沉浸态设置:

boolean threeButton = isThreeButtonNavBar(activity);
if (threeButton) {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
} else {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
}
public static boolean isThreeButtonNavBar(@NonNull Activity activity) {
return Settings.Global.getInt(activity.getContentResolver(), "force_fsg_nav_bar", 0) == 0;
}

1)创建浮窗Activity

创建一个新的Activity,并将其设置为浮窗样式。可以通过设置Activity的WindowManager.LayoutParams来实现浮窗效果。

public class FloatingWindowActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_floating_window);
// 设置Activity为浮窗样式
WindowManager.LayoutParams params = getWindow().getAttributes();
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.gravity = Gravity.TOP | Gravity.START;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
getWindow().setAttributes(params);
// 设置Activity为透明背景
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
// 添加Fragment
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, new FloatingFragment())
.commit();
}
}
}


2)创建浮窗Fragment

创建一个Fragment来管理浮窗的内容。Fragment可以包含任何你想要的UI组件。

public class FloatingFragment extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_floating, container, false);
// 在这里初始化你的UI组件
return view;
}
}


3)设置布局文件

这里实现一个最简单的布局样式activity_floating_window.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent">
</FrameLayout>


开发者资源

9.Jetpack Compose适配

Compose采用声明式编程模型,开发者只需描述UI的最终状态,无需手动管理视图树的创建与更新(如传统View中需频繁调用findViewById或setVisibility)。这种范式将逻辑与布局高度融合,代码量平均减少30%-69%,且通过Kotlin语言特性(如Lambda表达式)实现更简洁的布局描述。

传统开发需手动处理onSaveInstanceState或LiveData绑定,而Compose内置了响应式状态机制(如mutableStateOf),状态变化自动触发局部UI重组。通过remember等工具避免无效渲染,仅更新受影响的组件,对比传统View的全局重绘,性能优化更为智能。

Compose 的声明式特性允许开发者通过代码动态描述 UI 的呈现逻辑,无需依赖静态布局文件。例如,通过 ​窗口大小类(Window Size Classes)​​ 可快速定义“紧凑型”“中等型”和“展开型”断点,自动调整导航栏、列表-详情布局等组件形态(如手机底部导航切换为平板侧边导航),且原生提供部分控件可以帮助开发者快速进行大屏适配。

适配可参考文档《Jetpack Compose适配指导》

10.大屏外设适配

在折叠屏和平板上,用户经常使用的外设是键盘、鼠标和触控笔,因此应用适配外设可以给用户带来较好的用户体验。

10.1 键盘适配

 应用针对键盘常规的文字输入不需要做额外的适配,但是对于特殊按键功能定制和快捷键功能需要做单独的适配,因此对于键盘的适配主要分为单键和快捷键的适配处理。

10.1.1 单键

单键比如针对Enter、空格键或特殊的字母比如w、a、s和d控制方向等按键事件时,需要针对单个按键事件进行处理。适配代码参考如下:

 public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
sendMessage();
return true;
} else if (KeyEvent.KEYCODE_SPACE){
playOrPauseMedia();
return true;
} else {
return super.onKeyUp(keyCode, event);
}
}

10.1.2 快捷键

快捷键是指Ctrl+S(保存)、Ctrl+Z(撤销)等等组合按键,和电脑的使用习惯类似的快捷键功能实现。适配代码参考如下:

public boolean dispatchKeyShortcutEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_O) {
openFile(); // Ctrl+O, Shift+O, Alt+O
return true;
} else if(event.getKeyCode() == KeyEvent.KEYCODE_Z) {
if (event.isCtrlPressed()) {
if (event.isShiftPressed()) {
redoLastAction();
return true;

return super.dispatchKeyShortcutEvent(event);
}

键盘更多的适配指导可以参考Android官网:

https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input?hl=zh-cn

10.2 鼠标适配

鼠标适配主要包含左键和右键点击适配,左键点击适配需要实现View. onClick或View. onTouchEvent,和手指触碰操作处理事件一样,不需要特殊适配。右键点击适配需要实
现View. onGenericMotionEvent,参考代码如下:

@Override
public boolean onGenericMotionEvent(MotionEvent event) {
// 检查事件来源是否为鼠标
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
switch (event.getAction()) {
case MotionEvent.ACTION_BUTTON_PRESS:
// 检查是否为右键点击
if (event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
// 处理右键点击事件
handleRightClick(event);
return true;
}
break;
case MotionEvent.ACTION_BUTTON_RELEASE:
// 检查是否为右键释放
if (event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
// 处理右键释放事件
handleRightClickRelease(event);
return true;
}
break;
}
}
return super.onGenericMotionEvent(event);
}

鼠标更多的适配指导可以参考Android官网:

https://developer.android.com/develop/ui/views/touch-and-input/input-compatibility-on-large-screens?hl=zh-cn

10.3 触控笔适配

触控笔事件和屏幕触摸事件的处理流程一样,应用可以在View.dispatchTouchEvent或View.onTouchEvent中处理触控笔事件。MotionEvent中和触控笔相关的Api说明如下:

1、MotionEvent#getToolType()  

返回TOOL_TYPE_FINGER、TOOL_TYPE_STYLUS 或 TOOL_TYPE_ERASER,分别表示是否是触摸事件、触控笔事件或橡皮檫事件。

2、MotionEvent#getPressure()

报告施加到触控笔的物理压力

3、MotionEvent#getAxisValue()

与 MotionEvent.AXIS_TILT 和 MotionEvent.AXIS_ORIENTATION 一起使用,可提供触控笔的物理倾斜度和方向

触控笔更多的适配指导可以参考Android官网:

https://developer.android.com/develop/ui/views/touch-and-input/stylus-input?hl=zh-cn

备注:针对触控笔特殊的场景和需求,各硬件厂家在Android触控笔的基础上,针对触控笔做了定制,应用在不同的大屏设备上有特殊的需求可以接入各家的触控笔SDK,接入文档如下:

xiaomi:

https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1884

11.android新特性对大屏适配影响

这里介绍的Android新特性主要是指Android 15.0和16.0的新特性对应用的显示有比较大的影响,需要开发者注意且及时适配的特性。

11.1 Edge to Edge

此特性是Android 15.0提出,对于应用设置targetsdk为35且运行在Android 15设备上,应用汇默认采用边到边的显示模式。对于边对边的显示效果对比如下:

目标平台为 Android 14 且在 Android 15 设备上未采用边到边设计的应用。  

以 Android 15(API 级别 35)为目标平台且在 Android 15 设备上为端到端的应用。

从上面的对比可以看出,当应用的targetsdk和运行的设备满足条件后,会强制应用布局显示在状态栏和导航栏底部,因此系统栏可能会对应用布局产生遮挡影响功能使用和美观,需要应用针对此新特性进行适配。

如下是应用适配后的效果图:

应用适配边到边的效果

应用可以设置R.attr#windowOptOutEdgeToEdgeEnforcement为true来暂时停用这个新特性,但是当应用升级targetsdk到36时,此停用方法会被废弃,因此请尽快适配此新特性。

此特性详细的适配指导和注意事项可以参考Android官网:

https://developer.android.com/about/versions/15/behavior-changes-15?hl=zh-cn#window-insets

备注:此新特性对于直板机也生效,非大屏设备独有新特性。

11.2 Stable Configuration

此特性是Android 15.0提出,对于应用设置targetsdk为35且运行在Android 15设备上生效。此特性生效后,Configuration 不再排除系统栏。如果应用使用 Configuration 类中的屏幕尺寸进行布局计算,则应根据需要将其替换为更好的替代方案,例如适当的 ViewGroup、WindowInsets 或 WindowMetricsCalculator。

以下列表介绍了受此变更影响的字段:

1、Configuration.screenWidthDp 和 screenHeightDp 尺寸不再排除系统栏。

2、Configuration.smallestScreenWidthDp 会间接受到对 screenWidthDp 和 screenHeightDp 的更改的影响。

3、在接近方形的设备上,Configuration.orientation 会间接受到对 screenWidthDp 和 screenHeightDp 所做的更改的影响。

Display.getSize(Point) 会间接受到 Configuration 中的更改影响。从 API 级别 30 开始,此方法已被弃用。

4、从 API 级别 33 开始,Display.getMetrics() 就已经这样运作了。

如果应用计算系统栏的高度依赖Configuration对布局做避让,如果不适配此新特性,会出现边对边新特性一样的问题,即应用布局会被系统栏遮挡影响使用和美观。

此特性详细的适配指导和注意事项可以参考Android官网:

https://developer.android.com/about/versions/15/behavior-changes-15?hl=zh-cn#stable-configuration

备注:此新特性对于直板机也生效,非大屏设备独有新特性。

11.3 自适应布局

此特性在Android 16.0提出,应用的targetsdk等于36且运行在Android 16设备上生效。此特性主要是对最小宽度大于等于600dp的显示屏上生效,即在大屏设备上,系统会忽略应用对屏幕方向、尺寸可调整性和宽高比限制。因此当应用升级targetsdk到36后,针对大屏设备必须要适配全屏显示和横竖屏显示,系统会忽略应用固定比例显示和固定屏幕方向显示。

&nbspAndroid系统提供了暂时缓冲此特性生效的配置方式,目的是给应用一定的时间去适配此特性,配置方式可能在Android 17会取消,因此应用升级targetsdk后尽快进行大屏适配。

如需停用特定 activity,请声明 PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY 清单属性:

<activity ...>
<property
android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
android:value="true" />
...
</activity>


如果应用的太多部分不支持 Android 16,您可以在应用级别应用相同的属性,以完全停用该功能:

<application ...>
<property
android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
android:value="true" />
</application>


此特性详细的适配指导和注意事项可以参考Android官网:

https://developer.android.com/about/versions/16/behavior-changes-16?hl=zh-cn#adaptive-layouts

12.FAQ

12.1 如何适配屏幕挖孔?

目前主流手机大多屏幕有挖孔,应用需要避开挖孔区域显示内容。
适配挖孔,请参阅Android开发者指南中 支持刘海屏


12.2 如何识别厂商折叠屏设备?


建议应用通过支持resizeable对不同尺寸和比例的窗口进行适配,强烈不建议通过识别折叠屏的方式做特殊处理及UI适配。
若三方应用需要识别各厂商折叠屏设备,可通过以下方式识别。
注意!!!如无必要请避免针对具体设备形态进行特殊处理。

Xiaomi:

public static boolean isOPPOFold() {
boolean isFold = false;
try {
Class<?> cls = Class.forName("com.oplus.content.OplusFeatureConfigManager");
Method instance = cls.getMethod("getInstance");
Object configManager = instance.invoke(null);
Method hasFeature = cls.getDeclaredMethod("hasFeature", String.class);
Object object = hasFeature.invoke(configManager, "oplus.hardware.type.fold");
if (object instanceof Boolean) {
isFold = (boolean) object;
}
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
return isFold;
}


适配可参考本文,第一章 《屏幕兼容性》 第二章 《应用连续性》 内容。

12.3 如何识别厂商平板设备?

Xiaomi:

public static boolean isXiaomiTablet() {
try {
Class c = Class.forName("android.os.SystemProperties");
Method m = c.getMethod("get",String.class);
String type = String.valueOf(m.invoke(c,"ro.build.characteristics"));
return type.contains("tablet");
} catch (Exception e) {
e.printStackTrace();
}
return false;
}


12.4 如何识别折叠屏悬停状态?


悬停状态,实际上是屏幕半折态,仍然处于大屏下,vivo(从Android 12L开始)、OPPO和小米(从Android T版本开始)均支持google的JetPack去感知展开态和半折态和半折态区域,用于应用监听半折态做悬停布局。
可参考本文4.4节 《折叠屏状态监听和获取》


12.5 当应用未接入AndroidX库时,如何感知折叠屏悬停状态?


可参考文档《android T 无androidX实现悬停监听》


12.6 如何同时兼容Activity Embedding与厂商自研平行视窗?


当应用同时接入Activity Embedding与厂商自研平行视窗时,系统会根据ROM的版本优先支持原生的Activity Embedding,如果ROM版本不支持Activity Embedding,则支持自研的平行视窗。
此时应用需要在manifest中添加如下标识,并指定为 false 。

<application>
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE" android:value="false" />
</application>


文档内容是否有帮助?
有帮助
无帮助