Android小窗口模式,picture-in-picture(PIP画中画)的使用

1.介绍

Android8.0的时候推出了画中画模式,可以让Activity缩小显示在其他Activity上方。当初我维护的项目本身自己实现了这个功能,Android加入画中画之后两个功能并行,互相交互的时候出了一大堆问题。现在几乎所有的视频软件都加入了这个功能。使用方法十分简单,但是需要处理好AudioFocus的问题。
在这里插入图片描述
在这里插入图片描述

2.参数介绍

在Android 8.0时候,只需要调用Activity的
enterPictureInPictureMode();
或者enterPictureInPictureModeIfPossible() 即可

 public boolean enterPictureInPictureMode(@NonNull PictureInPictureParams params) {
        try {
            if (params == null) {
                throw new IllegalArgumentException("Expected non-null picture-in-picture params");
            }
            return ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken, params);
        } catch (RemoteException e) {
            return false;
        }
    }

进入的时候需要传入PictureInPictureParams类型参数。
现在高版本Android已经@Deprecated enterPictureInPictureMode()enterPictureInPictureMode(@NonNull PictureInPictureArgs args) 这两个方法。想要使用画中画必须传入参数。

我们先看一下PictureInPictureParams 的源码

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app;

import android.annotation.Nullable;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Rational;

import java.util.ArrayList;
import java.util.List;

/**
 * Represents a set of parameters used to initialize and update an Activity in picture-in-picture
 * mode.
 */
public final class PictureInPictureParams implements Parcelable {

    /**
     * Builder class for {@link PictureInPictureParams} objects.
     */
    public static class Builder {

        @Nullable
        private Rational mAspectRatio;

        @Nullable
        private List<RemoteAction> mUserActions;

        @Nullable
        private Rect mSourceRectHint;

        /**
         * Sets the aspect ratio.  This aspect ratio is defined as the desired width / height, and
         * does not change upon device rotation.
         *
         * @param aspectRatio the new aspect ratio for the activity in picture-in-picture, must be
         * between 2.39:1 and 1:2.39 (inclusive).
         *
         * @return this builder instance.
         */
        public Builder setAspectRatio(Rational aspectRatio) {
            mAspectRatio = aspectRatio;
            return this;
        }

        /**
         * Sets the user actions.  If there are more than
         * {@link Activity#getMaxNumPictureInPictureActions()} actions, then the input list
         * will be truncated to that number.
         *
         * @param actions the new actions to show in the picture-in-picture menu.
         *
         * @return this builder instance.
         *
         * @see RemoteAction
         */
        public Builder setActions(List<RemoteAction> actions) {
            if (mUserActions != null) {
                mUserActions = null;
            }
            if (actions != null) {
                mUserActions = new ArrayList<>(actions);
            }
            return this;
        }

        /**
         * Sets the source bounds hint. These bounds are only used when an activity first enters
         * picture-in-picture, and describe the bounds in window coordinates of activity entering
         * picture-in-picture that will be visible following the transition. For the best effect,
         * these bounds should also match the aspect ratio in the arguments.
         *
         * @param launchBounds window-coordinate bounds indicating the area of the activity that
         * will still be visible following the transition into picture-in-picture (eg. the video
         * view bounds in a video player)
         *
         * @return this builder instance.
         */
        public Builder setSourceRectHint(Rect launchBounds) {
            if (launchBounds == null) {
                mSourceRectHint = null;
            } else {
                mSourceRectHint = new Rect(launchBounds);
            }
            return this;
        }

        /**
         * @return an immutable {@link PictureInPictureParams} to be used when entering or updating
         * the activity in picture-in-picture.
         *
         * @see Activity#enterPictureInPictureMode(PictureInPictureParams)
         * @see Activity#setPictureInPictureParams(PictureInPictureParams)
         */
        public PictureInPictureParams build() {
            PictureInPictureParams params = new PictureInPictureParams(mAspectRatio, mUserActions,
                    mSourceRectHint);
            return params;
        }
    }

    /**
     * The expected aspect ratio of the picture-in-picture.
     */
    @Nullable
    private Rational mAspectRatio;

    /**
     * The set of actions that are associated with this activity when in picture-in-picture.
     */
    @Nullable
    private List<RemoteAction> mUserActions;

    /**
     * The source bounds hint used when entering picture-in-picture, relative to the window bounds.
     * We can use this internally for the transition into picture-in-picture to ensure that a
     * particular source rect is visible throughout the whole transition.
     */
    @Nullable
    private Rect mSourceRectHint;

    /** {@hide} */
    PictureInPictureParams() {
    }

    /** {@hide} */
    PictureInPictureParams(Parcel in) {
        if (in.readInt() != 0) {
            mAspectRatio = new Rational(in.readInt(), in.readInt());
        }
        if (in.readInt() != 0) {
            mUserActions = new ArrayList<>();
            in.readParcelableList(mUserActions, RemoteAction.class.getClassLoader());
        }
        if (in.readInt() != 0) {
            mSourceRectHint = Rect.CREATOR.createFromParcel(in);
        }
    }

    /** {@hide} */
    PictureInPictureParams(Rational aspectRatio, List<RemoteAction> actions,
            Rect sourceRectHint) {
        mAspectRatio = aspectRatio;
        mUserActions = actions;
        mSourceRectHint = sourceRectHint;
    }

    /**
     * Copies the set parameters from the other picture-in-picture args.
     * @hide
     */
    public void copyOnlySet(PictureInPictureParams otherArgs) {
        if (otherArgs.hasSetAspectRatio()) {
            mAspectRatio = otherArgs.mAspectRatio;
        }
        if (otherArgs.hasSetActions()) {
            mUserActions = otherArgs.mUserActions;
        }
        if (otherArgs.hasSourceBoundsHint()) {
            mSourceRectHint = new Rect(otherArgs.getSourceRectHint());
        }
    }

    /**
     * @return the aspect ratio. If none is set, return 0.
     * @hide
     */
    public float getAspectRatio() {
        if (mAspectRatio != null) {
            return mAspectRatio.floatValue();
        }
        return 0f;
    }

    /** @hide */
    public Rational getAspectRatioRational() {
        return mAspectRatio;
    }

    /**
     * @return whether the aspect ratio is set.
     * @hide
     */
    public boolean hasSetAspectRatio() {
        return mAspectRatio != null;
    }

    /**
     * @return the set of user actions.
     * @hide
     */
    public List<RemoteAction> getActions() {
        return mUserActions;
    }

    /**
     * @return whether the user actions are set.
     * @hide
     */
    public boolean hasSetActions() {
        return mUserActions != null;
    }

    /**
     * Truncates the set of actions to the given {@param size}.
     * @hide
     */
    public void truncateActions(int size) {
        if (hasSetActions()) {
            mUserActions = mUserActions.subList(0, Math.min(mUserActions.size(), size));
        }
    }

    /**
     * @return the source rect hint
     * @hide
     */
    public Rect getSourceRectHint() {
        return mSourceRectHint;
    }

    /**
     * @return whether there are launch bounds set
     * @hide
     */
    public boolean hasSourceBoundsHint() {
        return mSourceRectHint != null && !mSourceRectHint.isEmpty();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        if (mAspectRatio != null) {
            out.writeInt(1);
            out.writeInt(mAspectRatio.getNumerator());
            out.writeInt(mAspectRatio.getDenominator());
        } else {
            out.writeInt(0);
        }
        if (mUserActions != null) {
            out.writeInt(1);
            out.writeParcelableList(mUserActions, 0);
        } else {
            out.writeInt(0);
        }
        if (mSourceRectHint != null) {
            out.writeInt(1);
            mSourceRectHint.writeToParcel(out, 0);
        } else {
            out.writeInt(0);
        }
    }

    public static final Creator<PictureInPictureParams> CREATOR =
            new Creator<PictureInPictureParams>() {
                public PictureInPictureParams createFromParcel(Parcel in) {
                    return new PictureInPictureParams(in);
                }
                public PictureInPictureParams[] newArray(int size) {
                    return new PictureInPictureParams[size];
                }
            };
}

可以看到PictureInPictureParams是使用链式调用的建造者模式设计的,他的主要成员有
mAspectRatio

/**
         * Sets the aspect ratio.  This aspect ratio is defined as the desired width / height, and
         * does not change upon device rotation.
         *
         * @param aspectRatio the new aspect ratio for the activity in picture-in-picture, must be
         * between 2.39:1 and 1:2.39 (inclusive).
         *
         * @return this builder instance.
         */

高宽比,比例限定在2.39:1到1:2.39之间。

mUserActions

     /**
         * Sets the user actions.  If there are more than
         * {@link Activity#getMaxNumPictureInPictureActions()} actions, then the input list
         * will be truncated to that number.
         *
         * @param actions the new actions to show in the picture-in-picture menu.
         *
         * @return this builder instance.
         *
         * @see RemoteAction
         */

Action是一种控件,早期版本的Android GMS Setting中的item就是由Action组成的。
这里的userAction是RemoteAction类型,这个类型和Action没什么关系,但是是类似的东西,它是一个可序列化的对象,具体成员为

 	private final Icon mIcon;
    private final CharSequence mTitle;
    private final CharSequence mContentDescription;
    private final PendingIntent mActionIntent;
    private boolean mEnabled;

具体代码可看RemoteAction.java

简单来说这个就是当Activity进入画中画之后缩小的Activity下方会显示一个类似Android导航栏的东西,由几个按键组成,每个按键就是一个RemoteAction。将这个RemoteAction 列表在放入PictureInPictureParams参数中进入画中画的时候这些按钮就会显示。但是在手机端一般不需要,这几个键的功能一般是回到全屏显示,关闭画中画小窗口,切换小窗口大小,位置这种功能,一般手机端的APP只需要双击小窗口回到全屏这个功能。但是Android TV端是需要的,因为TV一般没有触摸屏,全靠遥控器,走的是onKey事件,需要使用按键来切换获得焦点的控件。

这里比较重要的是mActionIntent,这是一个PendingIntent类型的对象。这个PendingIntent定义了RemoteAction被触发后的行为。PendingIntent可以看作是对Intent的一个封装,但它不是立刻执行某个行为。可以使用getActivity, getActivities, getBroadcast, and getService方法获取可以启动Activity或者发送广播开启服务的PendingIntent对象。

扫描二维码关注公众号,回复: 8699846 查看本文章

PendingIntent官方文档

mSourceRectHint

 /**
         * Sets the source bounds hint. These bounds are only used when an activity first enters
         * picture-in-picture, and describe the bounds in window coordinates of activity entering
         * picture-in-picture that will be visible following the transition. For the best effect,
         * these bounds should also match the aspect ratio in the arguments.
         *
         * @param launchBounds window-coordinate bounds indicating the area of the activity that
         * will still be visible following the transition into picture-in-picture (eg. the video
         * view bounds in a video player)
         *
         * @return this builder instance.
         */

边界约束
官方文档上的介绍是
在这里插入图片描述
每个单词我都知道什么意思,但是我感觉它讲的不是人话。

大概意思是说Activity在进入画中画模式之后因为变小了,宽高比又可能和原先屏幕不同,所以有些画面显示不出来,设定一个区域。这个区域在进入画中画缩小了之后依然可见。这个区域要和前面设置的高宽比对应,这样才能显示的比较好。(我猜的)

然后我在代码中设置各种Rect的值发现对PIP叼影响都没有,根本猜不到这玩意干吗的。这叼文档不讲人话,代码也没效果,我真不知道这玩意干吗的,有知道的大神麻烦指点一下。

3.使用

最早的时候使用十分简单。
直接调用Activity的

  @Override
    public void enterPictureInPictureModeIfPossible() {
        if (mActivityInfo.supportsPictureInPicture()) {
            enterPictureInPictureMode();
        }
    }

就可以了。现在这个API没法调用了,可以通过

 @Deprecated
    public void enterPictureInPictureMode() {
        enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
    }

进入PIP,但是和上面那个API相比,如果你的Activity不支持PIP模式,会报异常出来。
所以你需要在清单文件中加上这样一句话

 android:supportsPictureInPicture="true"
 android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"

我在Activity中创建一个Button,点击这个Button调用enterPictureInPictureMode()

在这里插入图片描述
点击之后的效果
在这里插入图片描述

再单击一次小窗口
在这里插入图片描述

但是这个方法毕竟被Deprecated了,目前API28上还可以使用,说不定下个版本这个API就被取消了。

所以我们还是需要学会使用带有PictureInPictureParams 参数的
enterPictureInPictureMode(@NonNull PictureInPictureParams params) 方法

 Icon icon = Icon.createWithResource(mContext, R.mipmap.ic_launcher);
        Icon icon2 = Icon.createWithResource(mContext,R.mipmap.ic_launcher_round);
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 998, intent, PendingIntent.FLAG_ONE_SHOT);
        RemoteAction remoteAction = new RemoteAction(icon, "标题", "介绍", pendingIntent);
        RemoteAction remoteAction2 = new RemoteAction(icon2, "标题2", "介绍2", pendingIntent);

        ArrayList<RemoteAction> arrayList = new ArrayList<RemoteAction>();
        arrayList.add(remoteAction);
        arrayList.add(remoteAction2);
        Rational rational = new Rational(3, 7);//这里如果设置的值太大或者太小或报异常
        Rect rect = new Rect(0, 0, 0, 0);

        final PictureInPictureParams params = new PictureInPictureParams.Builder()
                .setActions(arrayList).setAspectRatio(rational)
                .setSourceRectHint(rect).build();


        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//                enterPictureInPictureMode();
                enterPictureInPictureMode(params);

            }
        });

Rational的比值需要在2.39:1到1:2.39之间否则

java.lang.IllegalArgumentException: enterPictureInPictureMode: Aspect ratio is too extreme (must be between 0.418410 and 2.390000).

效果:
在这里插入图片描述
单击小窗口后
在这里插入图片描述
可以看到有2个按钮,这就是我们设置的RemoteAction,点击后会触发PendingIntent。
中间的按钮是自带的最大化按钮,右上角也是自带的关闭按钮,左上为自带的设置按钮,这个在不同版本的Android上还有Android TV上都可能不相同。

哎刚准备讲改变小窗口size的,我记得Android 8.0 拿到beta版本的时候还是可以调节小窗口大小的,有3种size,结果现在用最新的AndroidQ已经不能调节了,不知道google又改了啥。然后我用EMUI10看b站和斗鱼都找不到PIP入口图标了,优酷和爱奇艺可以进入PIP,但是也不能调节小窗口大小。

言归正传,可以看到进入小窗口后显示的比例和原屏幕比例不一样,所以有的内容显示不出来,这个时候需要对UI进行调节。

比如你播放视频的话,你可以先让Video全屏播放,或者干脆全屏播放的时候才提供PIP入口,然后设置视频对应的Rational给PIP,也就是说在进入PIP前和退出PIP后是需要做一些页面调整的,而且比如你有计算UI坐标或者动态修改控件位置的逻辑,在进入PIP之前都需要处理掉,回到正常模式再还原,否则进入PIP之后再去获取这些就不准确了,整个UI会乱掉。
之前PIP功能刚上的时候QA给我开了很多这样的BUG。

现在还提供了了onPictureInPictureModeChanged()回调

  @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
        if (isInPictureInPictureMode){

        }else {

        }
    }

可以在这里处理PIP进入和退出的逻辑。禁用一些UI或者干脆给PIP模式加载一套新的layout。

4.注意

一些需要注意的点
首先一个Activity进入PIP的时候,缩小在其他应用上方显示,这个时候生命周期是走到了onPause(),但是没有走到OnStop(),这个有点像以前的VisibleBehind模式下的Activity生命周期。所以如果你在onPause的时候暂停播放,进入画中画之后就不会播放了。

AudioFocus问题,在8.0的时候我看到很多app小窗口的时候会去抢AudioFocus,我记得我曾经开了3个小窗口可以同时播放视频,还都有声音。但是现在的版本不行了,同时只有一个APP能获取到AudioFocus,而且主页面播放视频小窗口会自动暂停,而且同一时间也只能存活一个PIP进程。

发布了20 篇原创文章 · 获赞 15 · 访问量 2976

猜你喜欢

转载自blog.csdn.net/weixin_44666188/article/details/103945913