Picture-In-Picture 画中画模式
此代码为项目中使用,封装了PlayerView,相关进入PIP模式的代码也是在PlayerView中
准备工作
AndroidManifest.xml 中,在对应的Activity下添加:
android:supportsPictureInPicture="true"
android:launchMode="singleTask"
android:configChanges="screenLayout|orientation"
准备进入PIP模式
- Android 8.0 Oreo(API Level 26)允许活动启动画中画 Picture-in-picture(PIP)模式,因此开启前需要对当前版本进行判断。
- 内存较低的设备可能无法开启PIP,hasSystemFeature(PackageManager. FEATURE_PICTURE_IN_PICTURE) 用来检查以确保可以使用PIP。
//PlayerView中点击PIP按钮进入PIP模式
@Override
public void onPipModeClick() {
enterPipModel(context);
}
private PictureInPictureParams.Builder build;
public void enterPipModel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
Activity activity = getActivity(getContext());
//Android 10+ 关闭PIP权限后,此处调用checkOp检查会报错
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (AppOpsManager.MODE_ALLOWED == appOpsManager.unsafeCheckOpNoThrow(
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
context.getApplicationInfo().uid, context.getPackageName())) {
buildPIP(activity);
} else {
showNoPIPPermission(context, activity);
}
} else {
if (AppOpsManager.MODE_ALLOWED == appOpsManager.checkOp(
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
context.getApplicationInfo().uid, context.getPackageName())) {
buildPIP(activity);
} else {
showNoPIPPermission(context, activity);
}
}
}
}
/**
* 创建PIP Build
*
* @param activity
*/
private void buildPIP(Activity activity) {
//这里宽高比例写死为16:9,也可用播放器view的宽高
build.setAspectRatio(new Rational(16, 9));
Rect rect = new Rect();
getGlobalVisibleRect(rect);
build.setSourceRectHint(rect);
//进入PIP
activity.enterPictureInPictureMode(build.build());
//对播放器控制view的隐藏
playerControlView.forceControlWidgetGone();
//回复播放
resumePlay();
}
/**
* 提示PIP无权限并且跳转到设置界面
*
* @param context
* @param activity
*/
private void showNoPIPPermission(Context context, Activity activity) {
Toast.makeText(context, R.string.player_view_pip_permissions_tips, Toast.LENGTH_LONG).show();
try {
Intent intent = new Intent("android.settings.PICTURE_IN_PICTURE_SETTINGS", Uri.parse("package:" + activity.getPackageName()));
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
PIP模式更新自定义Action
项目需求是暂停、恢复播放、快进15s、快退15s,因此需要在播放器播放、暂停时调用此方法更新PIP模式按钮状态
//播放视频时,按钮为暂停样式
updatePictureInPictureActions(R.drawable.exo_icon_pause,
getActivity(getContext()).getString(R.string.cast_pause),
PIPControlUtils.CONTROL_TYPE_PAUSE, PIPControlUtils.CONTROL_PAUSE_REQUEST_CODE);
//暂停播放时,按钮为播放样式
updatePictureInPictureActions(R.drawable.exo_icon_play,
getActivity(getContext()).getString(R.string.cast_pause),
PIPControlUtils.CONTROL_TYPE_PLAY, PIPControlUtils.CONTROL_PLAY_REQUEST_CODE
);
/**
* Update the state of pause/resume/fastforward/rewind action item in Picture-in-Picture mode.
*
* @param iconId 暂停、播放icon id
* @param title action 标题
* @param controlType action 类型
* @param requestCode PendingIntent 请求码
*/
void updatePictureInPictureActions(
@DrawableRes int iconId, String title, int controlType, int requestCode
) {
//Android 8.0+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
//最终PIP模式下自定义按钮的顺序与添加进actions的action顺序相同
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (BuildCompat.isAtLeastS()) {
//Android 12 以上需要再添加PendingIntent.FLAG_MUTABLE,否则内部方法checkFlag()会报错
flags |= PendingIntent.FLAG_MUTABLE;
}
final ArrayList<RemoteAction> actions = new ArrayList<>();
//暂停、恢复播放的PendingIntent
final PendingIntent pausePlayIntent =
PendingIntent.getBroadcast(
getActivity(getContext()),
requestCode,
new Intent(PIPControlUtils.ACTION_MEDIA_CONTROL).putExtra(PIPControlUtils.EXTRA_CONTROL_TYPE, controlType),
flags);
//快进的PendingIntent
final PendingIntent fastForwardIntent =
PendingIntent.getBroadcast(
getActivity(getContext()),
PIPControlUtils.CONTROL_FAST_FORWARD_REQUEST_CODE,
new Intent(PIPControlUtils.ACTION_MEDIA_CONTROL).putExtra(PIPControlUtils.EXTRA_CONTROL_TYPE, PIPControlUtils.CONTROL_TYPE_FAST_FORWARD),
flags);
//快退的PendingIntent
final PendingIntent rewindIntent =
PendingIntent.getBroadcast(
getActivity(getContext()),
PIPControlUtils.CONTROL_REWIND_REQUEST_CODE,
new Intent(PIPControlUtils.ACTION_MEDIA_CONTROL).putExtra(PIPControlUtils.EXTRA_CONTROL_TYPE, PIPControlUtils.CONTROL_TYPE_REWIND),
flags);
/**
* 快退按钮的action
*/
RemoteAction rewindAction = new RemoteAction(
Icon.createWithResource(getActivity(getContext()), R.drawable.ic_rewind_15),
"rewind",
"rewind",
rewindIntent
);
// RemoteAction.setEnabled(boolean); 设置action是否可用,传false则icon变成灰色且不可点击
actions.add(rewindAction);
/**
* 暂停播放按钮的action
*/
final Icon icon = Icon.createWithResource(getActivity(getContext()), iconId);
actions.add(new RemoteAction(icon, title, title, pausePlayIntent));
/**
* 快进按钮的action
*/
RemoteAction fastForwardAction = new RemoteAction(
Icon.createWithResource(getActivity(getContext()), R.drawable.ic_fast_forward_15),
"fast_forward",
"fast_forward",
fastForwardIntent
);
// setEnabled() -> 可切换action对应的icon是否可用,不可用则为灰色切不能点击
actions.add(fastForwardAction);
//设置已自定义的actions
build.setActions(actions);
//设置画中画参数
getActivity(getContext()).setPictureInPictureParams(build.build());
}
对应的Activity
生命周期:
进入PIP模式:onPause()
从PIP模式回复全屏:onResume()
关闭PIP模式:onStop()
PIP模式下锁屏/解锁:onStop() / onStart()
//是否进入PIP模式
private var isEnteredPIPMode = false
//复写finish()方法
override fun finish() {
finishAndRemoveTask()
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration?
) {
//进入PIP时,会调用onPause(),此时lifecycle.currentState 变为Lifecycle.State.STARTED
//退出PIP时,会调用onStop(),此时lifecycle.currentState 变为Lifecycle.State.CREATED,即可做finish Activity的操作
//STARTED->:after onStart call;right before onPause call.
//CREATED->:after onCreate call;right before onStop call.
if (lifecycle.currentState == Lifecycle.State.CREATED) {
if (null != mReceiver){
unregisterReceiver(mReceiver)
mReceiver = null
}
finish()
}
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
isEnteredPIPMode = isInPictureInPictureMode
if (isInPictureInPictureMode) {
//进入PIP模式后,注册广播接收器以接受自定义Action
registerBroadcast()
}else{
if (null != mReceiver){
//退出PIP模式后,注销广播接收器,将mReceiver置空
unregisterReceiver(mReceiver)
mReceiver = null
}
}
}
//注册广播接收器
private fun registerBroadcast(){
mReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//当前Activity只需要接受来自PIP 自定义Action发出的广播
if (PIPControlUtils.ACTION_MEDIA_CONTROL != intent.action
) {
return
}
//获取PIP控制类型
var controlType = intent.getIntExtra(
PIPControlUtils.EXTRA_CONTROL_TYPE,
PIPControlUtils.CONTROL_TYPE_PLAY
)
when (controlType) {
//暂停-> 播放
PIPControlUtils.CONTROL_TYPE_PLAY -> //播放事件
//播放-> 暂停
PIPControlUtils.CONTROL_TYPE_PAUSE -> //暂停事件
//快进
PIPControlUtils.CONTROL_TYPE_FAST_FORWARD -> //快进事件
//快退
PIPControlUtils.CONTROL_TYPE_REWIND -> //快退事件
}
}
}
registerReceiver(mReceiver, IntentFilter(PIPControlUtils.ACTION_MEDIA_CONTROL))
}
override fun onStop() {
super.onStop()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
/**
* isInPictureInPictureMode -> false,画中画模式时点了关闭按钮调用的onStop
* 注意:Android 12版本退出PIP模式时isInPictureInPictureMode未变成false
* 因此在onPictureInPictureModeChanged()方法里去判断是不是退出PIP模式
*/
//isInPictureInPictureMode -> true,画中画模式时息屏调用的onStop
if (isInPictureInPictureMode) {
//此处可以处理进入PIP模式后息屏下是否需要暂停
}
}
}
PIPControlUtils
class PIPControlUtils {
companion object {
//PendingIntent action
const val ACTION_MEDIA_CONTROL = "media_control"
//PendingIntent extra name
const val EXTRA_CONTROL_TYPE = "control_type"
//PendingIntent extra resume play
const val CONTROL_TYPE_PLAY = 0x800
//PendingIntent extra pause
const val CONTROL_TYPE_PAUSE = 0x801
//PendingIntent extra fast forward
const val CONTROL_TYPE_FAST_FORWARD= 0x802
//PendingIntent extra rewind
const val CONTROL_TYPE_REWIND = 0x803
//Fast forward time millis
const val FAST_FORWARD_TIME = 15 * 1000
//Rewind time millis
const val REWIND_TIME = 15 * 1000
//PendingIntent play request code
const val CONTROL_PLAY_REQUEST_CODE = 0x800
//PendingIntent pause request code
const val CONTROL_PAUSE_REQUEST_CODE = 0x801
//PendingIntent fast forward request code
const val CONTROL_FAST_FORWARD_REQUEST_CODE = 0x802
//PendingIntent rewind request code
const val CONTROL_REWIND_REQUEST_CODE = 0x803
}
}