Picture-In-Picture 画中画模式

Picture-In-Picture 画中画模式

此代码为项目中使用,封装了PlayerView,相关进入PIP模式的代码也是在PlayerView中

准备工作

AndroidManifest.xml 中,在对应的Activity下添加:

android:supportsPictureInPicture="true"
android:launchMode="singleTask"
android:configChanges="screenLayout|orientation"

准备进入PIP模式

  1. Android 8.0 Oreo(API Level 26)允许活动启动画中画 Picture-in-picture(PIP)模式,因此开启前需要对当前版本进行判断。
  2. 内存较低的设备可能无法开启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
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值