在这篇文章中,我们将深入探讨在 Android 应用开发中一个非常实用且能显著提升用户体验的功能——画中画(Picture-in-Picture,简称 PIP)。你是否在使用谷歌地图导航时,注意到当按下 Home 键返回桌面时,导航画面并没有消失,而是变成了一个悬浮在屏幕右下角的小窗口?这就是典型的 PIP 模式应用场景。作为一名开发者,掌握这一功能将使你的应用在处理视频播放、视频通话或长时任务时更加专业和友好。
通过阅读本文,你将学到:
- 什么是 PIP 模式以及它的核心应用场景。
- 如何在 AndroidManifest.xml 中正确配置 PIP 支持。
- 如何通过代码动态控制 PIP 的进入与退出,包括宽高比的设置。
- 处理 PIP 模式下的 UI 变化和生命周期事件。
- 从实战角度出发,我们将通过完整的 Java 和 Kotlin 代码示例来加深理解。
什么是 PIP (画中画) 模式?
PIP 本质上是一种特殊类型的多窗口模式。在 Android 8.0(API 级别 26)及其更高版本中,系统允许 Activity 进入 PIP 模式。这种模式的主要目的是让用户能够同时处理两项任务:比如在看视频的同时回复消息,或者在导航的同时查看其他应用的信息。
当 Activity 进入 PIP 模式时,它会被缩小为一个悬浮窗口漂浮在屏幕的最顶层。通常情况下,这个窗口默认位于屏幕的右下角,但用户可以通过长按或拖动将其移动到屏幕的任意位置。
PIP 窗口的交互细节:
你可能已经注意到了,当我们在 PIP 窗口上点击(单机)时,窗口会暂时展开,并在中心显示一个全屏切换按钮,右上角显示一个关闭按钮。这些控件是系统自动提供的,我们无需手动去画这些按钮,这在开发中大大简化了我们的工作。
逐步实现指南
为了演示如何在应用中实现 PIP,我们将创建一个简单的 Demo 应用,包含一个按钮,点击后即可将应用切换到 PIP 模式。我们将从创建新项目开始,一步步讲解。
#### 步骤 1:创建一个新项目
首先,我们需要在 Android Studio 中创建一个新的项目。如果你还不熟悉操作流程,可以在 Android Studio 中选择 "New Project",然后选择 "Empty Views Activity" 或 "Basic Activity" 作为模板。为了演示方便,我们将语言环境设置为 Java 或 Kotlin 均可,下文中我会同时提供这两种语言的代码。
#### 步骤 2:声明画中画支持
默认情况下,系统并不会自动为我们的 Activity 开启 PIP 支持。我们需要在 INLINECODE7ad3d5a5 文件中进行显式声明。这是至关重要的一步,如果忘记配置,代码调用 INLINECODE6b8d3568 时将不会生效,甚至可能抛出异常。
请打开 INLINECODE6a8b8a56,找到你想要支持 PIP 的 Activity 标签,并添加 INLINECODE168a34a0 属性。
此外,还有一个非常重要的细节:ConfigChanges。当 Activity 进入 PIP 模式时,系统默认会触发一次配置变更,这会导致 Activity 被“销毁并重建”。为了防止应用重置 UI 状态(比如视频重新开始播放),我们需要告诉系统我们将自行处理这些布局变化。
我们需要添加 android:configChanges 属性,包含以下值:
-
screenSize:屏幕尺寸变化(PIP 时窗口变小)。 -
screenLayout:屏幕布局变化。 -
orientation:屏幕方向变化。 -
smallestScreenSize:最小屏幕尺寸变化(自 Android 7.0 起)。
配置代码如下:
#### 步骤 3:处理 activity_main.xml
接下来,我们需要设计一个简单的用户界面。为了模拟真实场景,我们通常是在播放视频或执行长任务时进入 PIP。这里我们简化操作,放置一个按钮,点击它就触发 PIP 模式。当然,如果你的应用是视频播放器,通常应该在用户点击“Home键”或“最小化”按钮时触发,但在 Demo 中我们使用显式点击来展示控制权。
修改 res/layout/activity_main.xml:
#### 步骤 4:处理 MainActivity 文件(核心逻辑)
现在,让我们进入最核心的部分:编写代码来触发 PIP。我们需要在按钮的点击事件中调用 INLINECODE3af05e18 方法。但在此之前,我们需要构建一个 INLINECODEbddd38f9 对象。
关键技术点:宽高比
PIP 窗口不能是任意形状的,它需要一个明确的宽高比。系统根据这个比例来决定悬浮窗口的大小。如果宽高比设置不当,窗口可能会变形。通常的做法是获取当前 View 的宽高或者屏幕的宽高作为参考。
让我们来看看具体的实现代码。
Java 实现:
package org.geeksforgeeks.demo;
import androidx.appcompat.app.AppCompatActivity;
import android.app.PictureInPictureParams;
import android.graphics.Point;
import android.os.Build;
import android.os.Bundle;
import android.util.Rational;
import android.view.Display;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private Button enterButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
enterButton = findViewById(R.id.enter_button);
enterButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 检查系统版本是否支持 PIP (Android 8.0+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 1. 获取屏幕尺寸以计算宽高比
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int width = size.x;
int height = size.y;
// 2. 创建 Rational 对象,用于定义宽高比
// 这里我们使用屏幕的宽高比,模拟全屏视频进入 PIP 的效果
Rational aspectRatio = new Rational(width, height);
// 3. 构建 PictureInPictureParams
PictureInPictureParams.Builder pipBuilder = new PictureInPictureParams.Builder();
pipBuilder.setAspectRatio(aspectRatio);
// 4. 进入 PIP 模式
enterPictureInPictureMode(pipBuilder.build());
} else {
Toast.makeText(MainActivity.this, "当前系统版本不支持画中画功能", Toast.LENGTH_SHORT).show();
}
}
});
}
}
Kotlin 实现:
package org.geeksforgeeks.demo
import android.app.PictureInPictureParams
import android.graphics.Point
import android.os.Build
import android.os.Bundle
import android.util.Rational
import android.view.Display
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var enterButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
enterButton = findViewById(R.id.enter_button)
enterButton.setOnClickListener {
// 使用 API 版本检查
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 1. 获取 Display 尺寸
val display: Display = windowManager.defaultDisplay
val size = Point()
display.getSize(size)
val width = size.x
val height = size.y
// 2. 定义宽高比
// Rational 类用于表示不可约分的分数,确保比例精确
val aspectRatio = Rational(width, height)
// 3. 使用 Kotlin 作用域函数构建参数
val pipParams = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
.build()
// 4. 执行进入 PIP 模式操作
enterPictureInPictureMode(pipParams)
} else {
Toast.makeText(this, "当前系统版本过低,无法使用画中画", Toast.LENGTH_SHORT).show()
}
}
}
}
进阶与实战:处理 PIP 模式下的逻辑
仅仅进入 PIP 模式是不够的。在真正的生产环境中,我们需要处理很多细节问题。例如,当应用进入 PIP 模式时,我们通常会隐藏不必要的 UI 元素(如“点赞”按钮、评论区等),只保留视频画面;当用户点击 PIP 窗口将应用恢复全屏时,我们又要把这些 UI 元素加回来。
让我们来看看如何监听 PIP 状态的变化。
#### 监听 PIP 模式变化
在 Java 中,我们可以重写 onPictureInPictureModeChanged 方法:
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
if (isInPictureInPictureMode) {
// 进入 PIP 模式:隐藏 UI 控件
// 例如:findViewById(R.id.comments_section).setVisibility(View.GONE);
// 如果使用 ExoPlayer 等播放器,这里应该隐藏控制器
getSupportActionBar()?.hide();
} else {
// 退出 PIP 模式:恢复 UI 控件
// 例如:findViewById(R.id.comments_section).setVisibility(View.VISIBLE);
getSupportActionBar()?.show();
}
}
在 Kotlin 中同样重写该方法:
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
// 当进入小窗口模式时,为了让界面简洁,我们隐藏非必要的 UI
// 这里可以隐藏标题栏、按钮等
enterButton.visibility = View.GONE
} else {
// 当回到全屏时,恢复 UI 显示
enterButton.visibility = View.VISIBLE
}
}
#### 最佳实践与常见陷阱
在开发过程中,我总结了一些实用的经验,希望能帮你少走弯路:
- 处理视频播放暂停:
当应用进入 PIP 模式时,通常是不希望暂停视频播放的(这正是 PIP 存在的意义)。但系统为了节省资源,可能会暂停某些 Activity。你需要确保在 onPause() 生命周期方法中,判断当前是否处于 PIP 模式。如果是,请不要暂停视频播放器。
@Override
protected void onPause() {
super.onPause();
// 如果不在 PIP 模式,才暂停播放
if (!isInPictureInPictureMode()) {
videoView.pause();
}
}
- 不要在 PIP 模式下执行耗时任务:
虽然 PIP 模式下 Activity 依然可见,但用户可能正在另一个全屏应用中操作。此时后台性能受限,应避免进行大量的文件读写或网络请求,以免影响前台应用的流畅度。
- API 版本兼容性: 始终检查
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O。在低版本手机上调用相关方法会导致应用崩溃。
- 自动返回: 在 Android 12 (API 31) 及更高版本中,如果你设置了
setAutoEnterEnabled(true),当用户按下 Home 键时,系统会自动将当前支持 PIP 的 Activity 转换为 PIP 模式,无需手动调用 enter 方法。这对于视频应用来说是一个很好的用户体验优化。
总结
通过这篇文章,我们深入探讨了 Android 画中画功能的实现细节。从 Manifest 的基础配置,到 Java 和 Kotlin 的代码实现,再到处理生命周期和 UI 状态的进阶技巧。正如我们所见,PIP 模式虽然强大,但实现起来并不复杂,关键在于正确设置宽高比和处理好 Activity 的状态变更。
建议你亲自运行一下上面的代码,观察应用进入 PIP 模式后的行为变化,特别是当你点击小窗口将它展开或关闭时。希望这篇指南能帮助你打造出体验更优秀的 Android 应用!如果你在开发中遇到任何问题,欢迎随时查阅官方文档或在社区进行交流。