Android画中画模式

您所在的位置:网站首页 安卓手机画中画视频实现步骤是什么 Android画中画模式

Android画中画模式

2024-07-13 00:04| 来源: 网络整理| 查看: 265

最近做做播放器,有个浮窗播放的需求,两种实现方式,一种是申请浮窗权限,创建浮窗参考 flowWindow,一种是采用画中画模式(8.0以上)

关于画中画

Android 8.0 Oreo(API Level 26)允许活动启动画中画 Picture-in-picture(PIP)模式。PIP 是一种特殊类型的多窗口模式,主要用于视频播放。PIP 模式已经可用于 Android TV,而 Android 8.0 则让该功能可进一步用于其他 Android 设备。 画中画利用 Android 7.0 中的多窗口模式 API 来提供固定的视频叠加窗口。要将画中画添加到您的应用中,您需要注册支持画中画的 Activity、根据需要将 Activity 切换为画中画模式,并确保当 Activity 处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。  

 

如何使用 声明对画中画的支持

默认情况下,系统不会自动为应用提供画中画支持。要想在应用中支持画中画,您可以通过将  android:supportsPictureInPicture  和  android:resizeableActivity  设置为  true ,在清单中注册视频 Activity。此外,指定您的 Activity 会处理布局配置更改,这样一来,在画中画模式转换期间发生布局更改时,您的 Activity 不会重新启动。  

      = Build.VERSION_CODES.N; if(videoPlayer!= null) {     videoPlayer.setSupportPipMode(isSupportPipMode); }

对单个播放 Activity 使用画中画模式

要确保将单个 Activity 用于视频播放请求并根据需要进入或退出画中画模式,请在清单中将 Activity 的android:launchMode 设置为 singleTask:

    = Build.VERSION_CODES.O) {         videoPlayer.setIsInPictureInPictureMode(true);         if (mPictureInPictureParamsBuilder == null) {             mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();         }         // Calculate the aspect ratio of the PiP screen. 计算video的纵横比         mVideoWith = videoPlayer.getCurrentVideoWidth();         mVideoHeight = videoPlayer.getCurrentVideoHeight();         if (mVideoWith != 0 && mVideoHeight != 0) {             //设置param宽高比,根据宽高比例调整初始参数             Rational aspectRatio = new Rational(mVideoWith, mVideoHeight);             mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio);         }         //进入pip模式         enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());     } }

进入 PIP 模式的最常见流程如下: 1. 从按钮触发     * onClicked (View),onOptionsItemSelected (MenuItem) 等等。 2. 有意的离开您的应用程序触发     * onUserLeaveHint ( ) 3. 从返回触发     * onBackPressed ( )  

在画中画期间处理界面

当 Activity 进入或退出画中画模式时,系统会调用 Activity. onPictureInPictureModeChanged() 或 Fragment. onPictureInPictureModeChanged()。 您应替换这些回调以重新绘制 Activity 的界面元素。请注意,在画中画模式下,您的 Activity 会在一个小窗口中显示。在画中画模式下,用户可能看不清小界面元素的详细信息,因此不会与这些界面元素互动。界面极简的视频播放 Activity 可提供出色的用户体验。Activity 应仅显示视频播放控件。在 Activity 进入画中画模式之前移除其他界面元素,并在 Activity 再次变为全屏时恢复这些元素:

    @Override     public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode, Configuration newConfig) {         if (isInPictureInPictureMode) {             // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.         } else {             // Restore the full-screen UI.             ...         }     }

在画中画模式下继续播放视频

当您的 Activity 切换到画中画模式时,系统会将该 Activity 置于暂停状态并调用 Activity 的 onPause() 方法。如果该 Activity 在画中画模式下暂停,则视频播放不得暂停,而应继续播放。 在 Android 7.0 及更高版本中,当系统调用 Activity 的 onStop() 时,您应暂停视频播放;当系统调用 Activity 的 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.             ...         }     }

切换视频/播放下一个时动态调画中画整宽高比例

/** * 视频尺寸变化(上一个下一个时),动态调整PIP 宽高比 * * @param with video宽度(非界面宽度) * @param height video高度(非界面高度) */ private void videoSizeChange(int with, int height) {     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (height != mVideoHeight || mVideoWith != with)) {         mVideoWith = with;         mVideoHeight = height;         if (mPictureInPictureParamsBuilder != null && mVideoWith != 0 && mVideoHeight != 0) {             //设置param宽高比,根据快高比例调整初始参数             Rational aspectRatio = new Rational(mVideoWith, mVideoHeight);             mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio);               //设置更新PictureInPictureParams             setPictureInPictureParams(mPictureInPictureParamsBuilder.build());         }     } }

进阶使用 添加自定义按钮: 方式一: 通过MediaSession达到如下图效果

(此处有关videoPlayer相关代码根据自己播放器灵活代入,仅供参考)

 

当 Activity 进入画中画模式后,它默认没有获得输入焦点。要在画中画模式下接收输入事件,请使用 MediaSession.setCallback() 。如需详细了解如何使用 setCallback(),请参阅显示“ 正在播放 ”卡片。 首先在进入小窗前初始化MediaSessionCompat  

private MediaSessionCompat mSession; public static final long MEDIA_ACTIONS_PLAY_PAUSE = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE; public static final long MEDIA_ACTIONS_ALL = MEDIA_ACTIONS_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;   private void initializeMediaSession() {     mSession = new MediaSessionCompat(this, TAG);     mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);     mSession.setActive(true);     MediaControllerCompat.setMediaController(this, mSession.getController());       MediaMetadataCompat metadata = new MediaMetadataCompat.Builder().build();     mSession.setMetadata(metadata);       MediaSessionCallback mMediaSessionCallback = new MediaSessionCallback(videoPlayer);     mSession.setCallback(mMediaSessionCallback);       int state = videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;     updatePlaybackState(state, MEDIA_ACTIONS_ALL, 0, 0); }

在MediaSessionCompat.Callback中设置自己的播放器逻辑响应

private class MediaSessionCallback extends MediaSessionCompat.Callback {       private LocalListVideoPlayer movieView;     private int indexInPlaylist;         public MediaSessionCallback(LocalListVideoPlayer movieView) {         this.movieView = movieView;         indexInPlaylist = 1;     }       @Override     public void onPlay() {         super.onPlay();         movieView.getGSYVideoManager().start();         movieView.setIsInPictureInPictureMode(true);         movieView.setCurrentState(GSYVideoView.CURRENT_STATE_PLAYING);         updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, 0, 0);     }       @Override     public void onPause() {         super.onPause();         movieView.getGSYVideoManager().pause();         movieView.setIsInPictureInPictureMode(true);         movieView.setCurrentState(GSYVideoView.CURRENT_STATE_PAUSE);         updatePlaybackState(PlaybackStateCompat.STATE_PAUSED, 0, 0);     }       @Override     public void onSkipToNext() {         super.onSkipToNext();         movieView.playNext();     }       @Override     public void onSkipToPrevious() {         super.onSkipToPrevious();         movieView.playLast();     } }   //更新按钮操作 private void updatePlaybackState(@PlaybackStateCompat.State int state, int position, int mediaId) {     if (mSession.getController().getPlaybackState() != null) {         long actions = mSession.getController().getPlaybackState().getActions();         updatePlaybackState(state, actions, position, mediaId);     } }   //初始化setPlaybackState private void updatePlaybackState(@PlaybackStateCompat.State int state, long playbackActions, int position, int mediaId) {     if (mSession != null) {         PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder()                 .setActions(playbackActions)                 .setActiveQueueItemId(mediaId)                 .setState(state, position, 1.0f);         mSession.setPlaybackState(builder.build());     } }

在自己播放器状态更新时更新界面元素

@Override public void onVideoStart() {     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {         if(isInPictureInPictureMode()) {             updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, 0, 0);         }     } }   @Override public void onVideoPause() {     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {         if(isInPictureInPictureMode()) {             updatePlaybackState(PlaybackStateCompat.STATE_PAUSED, 0, 0);         }     } } 方式2: 自定义按钮 (推荐)

(注意,按钮不超过三个,位置不可调节) 您还可以通过在进入画中画模式之前构建 PictureInPictureParams(使用 PictureInPictureParams.Builder. setActions())来明确指定自定义操作,并使用 enterPictureInPictureMode(android.app.PictureInPictureParams) 或 setPictureInPictureParams(android.app.PictureInPictureParams) 在进入画中画模式时传递这些参数。  

    首先自定义按钮初始化或刷新 private BroadcastReceiver mReceiver; private static final String ACTION_MEDIA_CONTROL = "media_control"; private static final String EXTRA_CONTROL_TYPE = "control_type"; private static final int CONTROL_TYPE_PLAY = 1; private static final int CONTROL_TYPE_PAUSE = 2; private static final int CONTROL_TYPE_LAST = 3; private static final int CONTROL_TYPE_NEXT = 4; private static final int REQUEST_TYPE_PLAY = 1; private static final int REQUEST_TYPE_PAUSE = 2; private static final int REQUEST_TYPE_LAST = 3; private static final int REQUEST_TYPE_NEXT = 4;   //进入画中画前判断状态,调用initPictureInPictureActions     private void initPictureInPictureActions() {         //int state = videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;         //STATE_PLAYING = 3  ; STATE_PAUSED = 2         if (videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING) {             updatePictureInPictureActions(R.drawable.gsy_play_video_icon_pause, "", CONTROL_TYPE_PLAY, REQUEST_TYPE_PLAY);         } else {             updatePictureInPictureActions(R.drawable.gsy_play_video_icon_play, "", CONTROL_TYPE_PAUSE, REQUEST_TYPE_PAUSE);         }     } /** * 刷新自定义按钮 (若是初始化,注意区分进入画中画前onpause状态) * * @param iconId * @param title * @param controlType * @param requestCode 注意!! 每个intent的requestCode必须不一样 */ void updatePictureInPictureActions(@DrawableRes int iconId, String title, int controlType, int requestCode) {     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {         if (mPictureInPictureParamsBuilder == null) {             mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();         }         final ArrayList actions = new ArrayList();         // This is the PendingIntent that is invoked when a user clicks on the action item.  You need to use distinct request codes for play and pause, or the PendingIntent won't be  updated.             //上一个         final PendingIntent intentLast = PendingIntent.getBroadcast(this, REQUEST_TYPE_NEXT, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_LAST), 0);         actions.add(new RemoteAction(Icon.createWithResource(this, R.drawable.gsy_play_video_icon_last), "", "", intentLast));         //暂停/播放         final PendingIntent intentPause = PendingIntent.getBroadcast(this, requestCode, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, controlType), 0);         actions.add(new RemoteAction(Icon.createWithResource(this, iconId), title, title, intentPause));         //下一个         final PendingIntent intentNext = PendingIntent.getBroadcast(this, REQUEST_TYPE_LAST, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_NEXT), 0);         actions.add(new RemoteAction(Icon.createWithResource(this, R.drawable.gsy_play_video_icon_next), "", "", intentNext));           mPictureInPictureParamsBuilder.setActions(actions);           // This is how you can update action items (or aspect ratio) for Picture-in-Picture mode. Note this call can happen even when the app is not in PiP mode.         setPictureInPictureParams(mPictureInPictureParamsBuilder.build());     } }

响应按钮发出的intent

@Override public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {     super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);     if (videoPlayer != null) {         isInPIPMode = isInPictureInPictureMode;         videoPlayer.setIsInPictureInPictureMode(isInPIPMode);     }     //自定义action形式     if (isInPictureInPictureMode) {         // Starts receiving events from action items in PiP mode.         mReceiver = new BroadcastReceiver() {             @Override             public void onReceive(Context context, Intent intent) {                 if (intent == null || !ACTION_MEDIA_CONTROL.equals(intent.getAction())) {                     return;                 }                 // This is where we are called back from Picture-in-Picture action                 final int controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0);                 try {                     switch (controlType) {                         case CONTROL_TYPE_PLAY:                             videoPlayer.getGSYVideoManager().start();                             videoPlayer.setIsInPictureInPictureMode(true);                             videoPlayer.setCurrentState(GSYVideoView.CURRENT_STATE_PLAYING);                             break;                         case CONTROL_TYPE_PAUSE:                             videoPlayer.getGSYVideoManager().pause();                             videoPlayer.setIsInPictureInPictureMode(true);                             videoPlayer.setCurrentState(GSYVideoView.CURRENT_STATE_PAUSE);                             break;                         case CONTROL_TYPE_LAST:                             videoPlayer.playLast();                             break;                         case CONTROL_TYPE_NEXT:                             videoPlayer.playNext();                             break;                     }                 } catch (Exception e) {                     e.printStackTrace();                 }             }         };         registerReceiver(mReceiver, new IntentFilter(ACTION_MEDIA_CONTROL));     } else {         // We are out of PiP mode. We can stop receiving events from it.         unregisterReceiver(mReceiver);         mReceiver = null;     } }

当播放状态改变时更新按钮功能

videoPlayer.setLocalPlayerCallback(new LocalListVideoPlayer.LocalPlayerCallback() {     @Override     public void clickPIPMode() {         enterPiPMode();     }     @Override     public void OnPrepareVideoSizeChanged(int with, int height) {         videoSizeChange(with, height);     }     @Override     public void surfaceDestroyed() {         handleSurfaceDestroyed();     }     @Override     public void onVideoStart() {         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {           //自定义action刷新-开始播放-按钮替换为暂停           updatePictureInPictureActions(R.drawable.gsy_play_video_icon_pause, "", CONTROL_TYPE_PLAY, REQUEST_TYPE_PLAY);         }     }     @Override     public void onVideoPause() {         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {         //自定义action刷新-暂停播放,按钮替换为开始         updatePictureInPictureActions(R.drawable.gsy_play_video_icon_play, "", CONTROL_TYPE_PAUSE, REQUEST_TYPE_PAUSE);         }

关于浮窗关闭后仍有声音,无法获取浮窗关闭通知 方式一:监听SurfaceView-surfaceDestroyed()

在官方demo中,采用mediaSession方式, 以surfaceview的 surfaceDestroyed()回调关闭播放器 我采用的gsy播放器(同bilibili播放器),无法监听画中画浮窗关闭,采用如下的方法 在播放界面底层创建一个空的emptySurfaceView,通过callback获知浮窗被手动关闭 (此方法有个缺陷:在锁屏时也会回调此方法)  

private SurfaceView emptySurfaceView; .... emptySurfaceView = findViewById(R.id.emtpy_surface); emptySurfaceView         .getHolder()         .addCallback(                 new SurfaceHolder.Callback() {                     @Override                     public void surfaceCreated(SurfaceHolder holder) {                     }                     @Override                     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {                     }                     @Override                     public void surfaceDestroyed(SurfaceHolder holder) {                         if(mLocalPlayerCallback != null) {                             mLocalPlayerCallback.surfaceDestroyed();                         }                     }                 });

方式2:通过进入/退出/关闭画中画VideoActivity的生命周期判断(推荐)

操作画中画时VideoActivity相关生命周期梳理: 

进入画中画--onPause 

画中画返回全屏--OnResume

关闭画中画--onStop

全屏播放状态下下锁屏/解锁 onPause ,onStop /  onStart,onResume

画中画状态下下锁屏/解锁 onStop /  onStart  

//是否支持pip画中画小窗模式(自行判断赋值时机) protected boolean isSupportPipMode = false; //是否已经在画中画模式(自行判断赋值时机) public boolean isInPIPMode = false; //是否点击进入过画中画模式--用于判断程序在后台时,由画中画返回全屏后退出,是否启动首页activity,以及onstop配合判断是否点击进入过画中画且在画中画模式 public boolean isEnteredPIPMode = false;   @Override protected void onResume() {     super.onResume();     //画中画返回全屏会执行onresume     isEnteredPIPMode = false; }   @Override protected void onStop() {     super.onStop();     //备注: 在画中画模式下,onStop执行时, 若是关闭画中画,isInPictureInPictureMode()=false ; 若是锁屏,isInPictureInPictureMode()=true ; 判断锁屏isLockPage()一直为false     boolean inPictureInPictureMode = false;     if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {         inPictureInPictureMode = isInPictureInPictureMode();     }     if (BuildConfig.DEBUG) {         Log.i(TAG, "onStop -- inPictureInPictureMode=" + inPictureInPictureMode + " ,isEnteredPIPMode=" + isEnteredPIPMode + " ,isInPIPMode=" + isInPIPMode);     }     if (!inPictureInPictureMode && isInPIPMode && isEnteredPIPMode) {         //满足此条件下认为是关闭了画中画界面         if (BuildConfig.DEBUG) {             Log.w(TAG, "onStop -- 判断为PIP下关闭画中画");         }         handleSurfaceDestroyed();         return;     }     if (inPictureInPictureMode && isInPIPMode && isEnteredPIPMode && videoPlayer != null) {         //满足此条件下认为是画中画模式下锁屏         videoPlayer.onVideoPause();         isPause = true;         if (BuildConfig.DEBUG) {             Log.w(TAG, "onStop -- 判断为PIP下锁屏");         }     } }

关于开启浮窗关闭后显示在最近任务列表 manifest添加 android:excludeFromRecents="true"

参考:关于Android TaskAffinity的那些事儿

From Picture-in-Picture activity to Back-Stack activity not working in android?  

关于APP进入后台,播放完成后吊起主页activity,主页activity也进入浮窗模式(部分机型偶现)

主页设置 android:supportsPictureInPicture="false"无效

方案:采用遍历tasks,task.moveToFront() - task.moveToFront(); 避免采用startActivity方法使应用回到前台

参考:Launching Intent from notification opening in picture-in-picture window  

public static void moveLauncherTaskToFront(Context context) {       ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);     assert activityManager != null;     final List appTasks = activityManager.getAppTasks();     for (ActivityManager.AppTask task : appTasks) {         final Intent baseIntent = task.getTaskInfo().baseIntent;         final Set categories = baseIntent.getCategories();         if (categories != null && categories.contains(Intent.CATEGORY_LAUNCHER)) {             task.moveToFront();             return;         }     } }

判断获取用户是否关闭了应用画中画模式

当您的应用处于画中画模式时,画中画窗口中的视频播放可能会对其他应用(例如,音乐播放器应用或语音搜索应用)造成音频干扰。为避免出现此问题,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果您在处于画中画模式时收到音频焦点丢失通知,请暂停或停止视频播放。  

//音频焦点的监听 protected AudioManager mAudioManager; mAudioManager = (AudioManager) getActivityContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);   /** * 监听是否有外部其他多媒体开始播放 */ protected AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {     @Override     public void onAudioFocusChange(int focusChange) {         switch (focusChange) {             case AudioManager.AUDIOFOCUS_GAIN:                 //获得了Audio Focus                 onGankAudio();                 break;             case AudioManager.AUDIOFOCUS_LOSS:                 //失去了Audio Focus,并将会持续很长的时间-暂停音频                 onLossAudio();                 break;             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:                 //暂时失去Audio Focus,并会很快再次获得                 onLossTransientAudio();                 break;             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:                 //暂时失去AudioFocus,但是可以继续播放,不过要在降低音量                 onLossTransientCanDuck();                 break;         }     } };

文章转载:

总结系列-Android画中画模式-看这篇就够啦



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3