Android悬浮窗口-画中画功能

效果展示

前言

从 Android 8.0(API 级别 26)开始,Android 允许活动以画中画 (PiP) 模式启动。PiP 是一种特殊类型的多窗口模式,主要用于视频播放。它允许用户在固定在屏幕一角的小窗口中观看视频,同时在应用程序之间导航或浏览主屏幕上的内容。
PiP 利用 Android 7.0 中提供的多窗口 API 来提供固定的视频叠加窗口。要将 PiP 添加到您的应用程序,您需要注册支持 PiP 的 Activity,根据需要将您的 Activity 切换到 PiP 模式,并确保 UI 元素被隐藏并且当 Activity 处于 PiP 模式时视频播放继续。
PiP 窗口出现在屏幕的最顶层,位于系统选择的角落。

用户如何与画中画窗口交互

用户可以将画中画窗口拖到另一个位置。从 Android 12 开始,用户还可以:


单击窗口可显示全屏切换、关闭按钮、设置按钮和应用程序提供的自定义操作(例如,播放控件)。
双击窗口可在当前 PiP 大小和最大 PiP 大小之间切换。通过将窗口拖动到左边缘或右边缘来隐藏窗口;要取消隐藏窗口,请点击隐藏窗口的可见部分或将其拖出。
使用捏合缩放调整画中画窗口的大小。

应用控制当前活动何时进入画中画模式。这里有些例子:


当用户点击主页按钮(在按钮导航模式下)或向上滑动到主页(在手势导航模式下)时,活动可以进入画中画模式。(这就是 Google 地图在用户同时运行另一个活动时继续显示方向的方式。)
当用户从视频返回浏览其他内容时,您的应用可以将视频移至画中画模式。
当用户观看一集内容的结尾时,您的应用可以将视频切换到画中画模式。主屏幕显示有关该系列下一集的宣传或摘要信息。
应用程序可以为用户提供一种在观看视频时排队其他内容的方式。视频继续以画中画模式播放,而主屏幕显示内容选择活动。

声明画中画支持

默认情况下,系统不会自动支持应用程序的画中画。如果想在应用程序中支持画中画,请在清单中注册视频活动,方法是设置 android:supportsPictureInPicturetrue. 此外,指定 Activity 处理布局配置更改,以便在 PiP 模式转换期间发生布局更改时 Activity 不会重新启动。

<activity
    android:name=".TestViewActivity"
    android:theme="@style/Theme.PictureInPicture"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    android:supportsPictureInPicture="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

将您的活动切换为画中画

要进入画中画模式,活动必须调用 enterPictureInPictureMode(). 例如,当用户单击应用程序 UI 中的专用按钮时,以下代码会将Activity切换到画中画模式:

private void minimize() {
        // Calculate the aspect ratio of the PiP screen.
        PictureInPictureParams.Builder mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
        Rational aspectRatio = new Rational(binding.videoView.getWidth(), binding.videoView.getHeight());
        mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
        enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
    }

如果希望包含将活动切换到画中画模式而不是进入后台的逻辑。例如,如果用户在应用程序导航时按下主页或最近按钮,谷歌地图就会切换到画中画模式。可以通过覆盖来捕获这种情况 onUserLeaveHint()

@Override
public void onUserLeaveHint () {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode();
    }
}

从 Android 12 开始,您可以使用该 setAutoEnterEnabled 标志在手势导航模式下向上滑动到主页时提供更流畅的画中画模式转换。

要实现此功能:

        1.用于setAutoEnterEnabled构造PictureInPictureParams.Builder,如下:

setPictureInPictureParams(new PictureInPictureParams.Builder()
    .setAspectRatio(aspectRatio)
    .setSourceRectHint(sourceRectHint)
    .setAutoEnterEnabled(true)
    .build());

        2.setPictureInPictureParams最新 PictureInPictureParams消息。应用不应等待 onUserLeaveHint回调(就像在 Android 11 中所做的那样)。例如,setPictureInPictureParams如果宽高比发生变化,应用程序可能希望在第一次播放和任何后续播放时调用。

        3.setAutoEnterEnabled(false)根据需要调用。例如,如果当前播放处于暂停状态,则视频应用进入 PiP 可能不是最佳选择。

画中画期间处理 UI

当activity进入或退出画中画模式时,系统调用 Activity.onPictureInPictureModeChanged() 或Fragment.onPictureInPictureModeChanged()。应该重写这些回调以重绘活动的 UI 元素。在画中画模式下,活动会显示在一个小窗口中,应用程序处于画中画模式时,用户无法与应用程序的 UI 元素进行交互,并且可能难以看到小的 UI 元素的详细信息。具有最少 UI 的视频播放活动可提供最佳用户体验。如果应用需要为画中画提供自定义操作,请参阅本文档中的添加控件。在 Activity 进入 PiP 之前移除其他 UI 元素,并在您的 Activity 再次变为全屏时恢复它们:

@Override
public void onPictureInPictureModeChanged(
        boolean isInPictureInPictureMode, Configuration configuration) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, configuration);
}

退出画中画模式时支持更流畅的动画

从 Android 12 开始,该SourceRectHint 标志现在被重复使用以在退出画中画模式时实现更流畅的动画。退出时,系统使用当前可用的动画创建动画 sourceRectHint,无论是Rect用于进入画中画的原始动画还是Rect应用程序提供的更新动画。

要实现此功能,请按如下方式:

1.继续构建PictureInPictureParams和以sourceRectHint 获得aspectRatio 平滑的进入动画。

2.如有必要,请sourceRectHint 在系统开始退出转换之前更新。当系统即将退出画中画模式时,活动的视图层次结构将布置到其目标配置(例如,全屏)。应用程序可以将布局更改侦听器附加到其根视图或目标视图(例如视频播放器视图)以检测事件并在动画开始之前更新 sourceRectHint。

// Listener is called right after the user exits PiP but before
// animating.
playerView.addOnLayoutChangeListener((v, left, top, right, bottom,
                     oldLeft, oldTop, oldRight, oldBottom) -> {
    if (left != oldLeft || right != oldRight || top != oldTop
            || bottom != oldBottom) {
       // The playerView’s bounds changed, update the source hint rect to
       // reflect its new bounds.
       final Rect sourceRectHint = new Rect();
       playerView.getGlobalVisibleRect(sourceRectHint);
       setPictureInPictureParams(
               new PictureInPictureParams.Builder()
               .setSourceRectHint(sourceRectHint)
               .build());
    }
});

添加控件

当用户打开窗口的菜单时,画中画窗口可以显示控件(通过在移动设备上点击窗口,或从电视遥控器中选择菜单。)

如果一个应用程序有一个活动的媒体会话,那么将出现播放、暂停、下一个和上一个控件。

还可以在进入 PiP 模式之前通过 build PictureInPictureParams with显式指定自定义操作PictureInPictureParams.Builder.setActions() ,并在进入 PiP 模式时使用 enterPictureInPictureMode(android.app.PictureInPictureParams) or传递参数setPictureInPictureParams(android.app.PictureInPictureParams)。不能超过最大数getMaxNumPictureInPictureActions()

禁用非视频内容的无缝调整大小

Android 12 添加了setSeamlessResizeEnabled 标志,当在画中画窗口中调整非视频内容的大小时,它提供了更加平滑的淡入淡出动画。以前,在画中画窗口中调整非视频内容的大小可能会产生不和谐的视觉伪影。

setSeamlessResizeEnabled标志true默认设置为向后兼容。将此设置保留为true用于视频内容,并将其更改为false用于非视频内容。

要禁用非视频内容的无缝调整大小:

  setPictureInPictureParams(new PictureInPictureParams.Builder()
          .setSeamlessResizeEnabled(false)
          .build());

在画中画中继续播放视频

当活动切换到画中画时,系统会将活动置于暂停状态并调用活动的 onPause()方法。如果活动在画中画模式下暂停,则不应暂停视频播放,并且应继续播放。

在 Android 7.0 及更高版本中,当系统调用 Activity onStop()和 onStart(). 通过这样做,可以避免在 onPause() 中检查应用是否处于画中画模式并显式继续播放。

如果必须在onPause()实现中暂停播放,请通过适当地调用和处理播放来检查画中画模式isInPictureInPictureMode(),例如:

@Override
public void onPause() {
    // If called while in PiP mode, do not pause playback
    if (isInPictureInPictureMode()) {
        // Continue playback
        ...
    } else {
        // Use existing playback logic for paused Activity behavior.
        ...
    }
}

当活动从画中画模式切换回全屏模式时,系统会恢复活动并调用 onResume()方法。

使用单个播放活动进行画中画

在应用程序中,用户可能会在主屏幕上浏览内容时选择新视频,而视频播放活动处于画中画模式。以全屏模式在现有播放活动中播放新视频,而不是启动可能会使用户感到困惑的新活动。

为确保单个活动用于视频播放请求并根据需要切换到或退出画中画模式,请在清单中将活动设置 android:launchModesingleTask

<activity android:name="VideoActivity"
    ...
    android:supportsPictureInPicture="true"
    android:launchMode="singleTask"
    ...

在活动中,覆盖 onNewIntent() 并处理新视频,如果需要,停止任何现有视频播放。

最佳实践

在 RAM 较低的设备上可能会禁用 PiP。应用使用 PiP 之前,请通过调用 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).

画中画适用于播放全屏视频的活动。将活动切换到画中画模式时,请避免显示除视频内容之外的任何内容。跟踪活动何时进入画中画模式并隐藏 UI 元素,如在画中画 期间处理 UI 中所述。

当活动处于画中画模式时,默认情况下它不会获得输入焦点。要在 PiP 模式下接收输入事件,请使用 MediaSession.setCallback(). 有关使用的更多信息,setCallback()请参阅 显示正在播放的卡片

当应用处于画中画模式时,画中画窗口中的视频播放可能会导致音频干扰其他应用,例如音乐播放器应用或语音搜索应用。为避免这种情况,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果 PiP 模式下收到音频焦点丢失通知,请暂停或停止视频播放。

当应用即将进入画中画时,请注意只有最上面的活动才会进入画中画。在某些情况下,例如在多窗口设备上,现在可能会显示下面的活动,并与画中画活动一起再次可见。应该相应地处理这种情况,包括下面获取onResume()onPause()回调的活动。用户也可能与活动交互。例如,如果显示了一个视频列表活动并且在画中画模式下正在播放视频活动,用户可能会从列表中选择一个新视频,并且画中画活动应该相应地更新。

Demo

activity

import android.app.PictureInPictureParams;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.util.Rational;
import android.view.View;
import android.widget.MediaController;

import com.mz.demo.databinding.ActivityTestViewBinding;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;

public class TestViewActivity extends AppCompatActivity {
    private ActivityTestViewBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityTestViewBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        binding.button.setOnClickListener(v -> {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                minimize();
            }
        });
        initData();
    }

    private void initData() {
        MediaController mediaController = new MediaController(this);
        binding.videoView.setMediaController(mediaController);
        mediaController.setMediaPlayer(binding.videoView);
        binding.videoView.setVideoPath("https://stream7.iqilu.com/10339/upload_transcode/202002/18/20200218114723HDu3hhxqIT.mp4");
        binding.videoView.start();
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    private void minimize() {
        // Calculate the aspect ratio of the PiP screen.
        PictureInPictureParams.Builder mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
        Rational aspectRatio = new Rational(binding.videoView.getWidth(), binding.videoView.getHeight());
        mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
        enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
    }

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

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            adjustFullScreen(getResources().getConfiguration());
        }
    }

    private void adjustFullScreen(Configuration config) {
        final WindowInsetsControllerCompat insetsController =
                ViewCompat.getWindowInsetsController(getWindow().getDecorView());
        if (insetsController == null)
            return;
        if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            insetsController.hide(WindowInsetsCompat.Type.systemBars());
            binding.button.setVisibility(View.GONE);
        } else {
            insetsController.show(WindowInsetsCompat.Type.systemBars());
            binding.button.setVisibility(View.VISIBLE);
        }
    }

    @Override
    public void onPictureInPictureModeChanged(
            boolean isInPictureInPictureMode, Configuration configuration) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, configuration);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        binding.videoView.stopPlayback();
    }
}

xml布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".TestViewActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="w,9:16"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

PS:acvitiy的style设置成无actionBar的

<style name="Theme.PictureInPicture" parent="Theme.MaterialComponents.DayNight.NoActionBar"/>

清单文件中这样添加:

<activity
    android:name=".TestViewActivity"
    android:theme="@style/Theme.PictureInPicture"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    android:supportsPictureInPicture="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

猜你喜欢

转载自blog.csdn.net/mozushixin_1/article/details/125282612