最近做做播放器,有个浮窗播放的需求,两种实现方式,一种是申请浮窗权限,创建浮窗参考 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 不会重新启动。

<activity android:name="VideoActivity"
        android:resizeableActivity="true"
        android:supportsPictureInPicture="true"
        android:configChanges=
            "screenSize|smallestScreenSize|screenLayout|orientation"
        ...

低内存设备可能无法使用画中画模式。在应用使用画中画之前,请务必通过调用hasSystemFeature(PackageManager. FEATURE_PICTURE_IN_PICTURE) 进行检查以确保可以使用画中画。

isSupportPipMode = getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
if(videoPlayer!= null) {
    videoPlayer.setSupportPipMode(isSupportPipMode);
}

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

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

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

在您的 Activity 中,替换 onNewIntent() 并处理新的视频,从而根据需要停止任何现有的视频播放。

将您的 Activity 切换到画中画模式

要进入画中画模式,Activity 必须调用 enterPictureInPictureMode()。例如,以下代码会在用户点击应用界面中的专用按钮时,将 Activity 切换到画中画模式:

/**
* 进入画中画模式
*/
private PictureInPictureParams.Builder mPictureInPictureParamsBuilder;
private void enterPiPMode() {
    if (videoPlayer == null) {
        return;
    }
    if (Build.VERSION.SDK_INT >= 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<RemoteAction> 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<ActivityManager.AppTask> appTasks = activityManager.getAppTasks();
    for (ActivityManager.AppTask task : appTasks) {
        final Intent baseIntent = task.getTaskInfo().baseIntent;
        final Set<String> 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画中画模式-看这篇就够啦

Android画中画模式-看这篇就够啦相关推荐

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

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

  2. Android 新特性 看这篇就够了

    Android 9.0版本 新功能 Android 9.0的新功能包括:谷歌统一推送升级.深度集成Project Treble模式.更加封闭.原生支持通话录音等. 1.全面屏的全面支持 [5] 2.通 ...

  3. Android悬浮窗看这篇就够了

    目录 悬浮窗的基本原理 动态添加View 悬浮窗原理 应用内悬浮窗 应用内悬浮窗实现流程 效果 应用外悬浮窗(有局限性) 效果 悬浮窗权限的适配 权限配置和请求 LayoutParam的坑!!!! 无 ...

  4. Android原生TabLayout使用全解析,看这篇就够了

    前言 为什么会有这篇文章呢,是因为之前关于TabLayout的使用陆陆续续也写了好几篇了,感觉比较分散,且不成体系,写这篇文章的目的就是希望能把各种效果的实现一次性讲齐,所以也有了标题的「看这篇就够了 ...

  5. React入门看这篇就够了

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: 很多值得了解的细节. 原文:React入门看这篇就够了 作者:Random Fundebug经授权转载,版权归原作者所 ...

  6. Handler原理剖析,看这篇就够了

    Handler原理剖析,看这篇就够了 本篇文章将会对Handler进行深层次的剖析,结合关系剖析图.代码走向剖析图以及10个常见问题,希望看完文章的同学都能有所收获,加深对Handler的了解! 一. ...

  7. uiautomation遍历windows所有窗口_万字长文!滑动窗口看这篇就够了!

    大家好,我是小浩.今天是小浩算法 "365刷题计划" 滑动窗口系列 - 整合篇.之前给大家讲解过一些滑动窗口的题目,但未作系统整理. 所以我就出了这个整合合集,整合工作中除了保留原 ...

  8. .NET Core实战项目之CMS 第二章 入门篇-快速入门ASP.NET Core看这篇就够了

    本来这篇只是想简单介绍下ASP.NET Core MVC项目的(毕竟要照顾到很多新手朋友),但是转念一想不如来点猛的(考虑到急性子的朋友),让你通过本文的学习就能快速的入门ASP.NET Core.既 ...

  9. 史上最全!用Pandas读取CSV,看这篇就够了

    导读:pandas.read_csv接口用于读取CSV格式的数据文件,由于CSV文件使用非常频繁,功能强大,参数众多,因此在这里专门做详细介绍. 作者:李庆辉 来源:大数据DT(ID:hzdashuj ...

最新文章

  1. 浅谈ClickableSpan , 实现TextView文本某一部分文字的点击响应
  2. mysql 重要监控参数_mysql 的重要参数,监控需要
  3. 802.11协议中帧控制域中To DS and From DS 比特位的含义
  4. 如何在客户端终止一个已经发出的HTTP请求
  5. 【AI视野·今日CV 计算机视觉论文速览 第196篇】Wed, 12 May 2021
  6. Arcgis for js开发之直线、圆、箭头、多边形、集结地等绘制方法
  7. 国内5家云服务厂商 HTTPS 安全性测试横向对比
  8. mac dmg包签名及公证
  9. 侯圣文大数据体验课笔记,大数据基础,离线数仓,实时计算
  10. usb摄像头android录像软件,USB摄像头app
  11. 不用Home Assistant让小米智能家居接入HomeKit
  12. 内存管理参数zone_reclaim_mode分析
  13. return 的含义
  14. 通用数据保护条例GDPR今日起正式生效,不会影响机器学习
  15. 08:go语言数字类型
  16. 自己动手模仿 springmvc 写一个 mvc框架
  17. 764. 最大加号标志
  18. Centos7 分离部署lnmp+discuz+wordpress 及Redis
  19. 如果王自如支持友推,而罗永浩不支持的话......
  20. 投研报告 - Polkadex(PDEX

热门文章

  1. Kyligence 荣获“高新技术企业”认证称号
  2. Excel中日期、数字、中文大写金额等转文本
  3. 国家信息安全水平考试NISP一级模拟题(04)
  4. 凭什么杀程序员祭天?
  5. VisualFreeBasic:VisualBasic6望尘莫及之变量
  6. C# Label 通过Panel中的ScrollBar实现滑动条
  7. #边学边记 必修4 高项:对事的管理 第5章 项目成本管理 之 制订成本管理计划
  8. c++ 进制转换 十六进制转八进制
  9. 电磁阀单电控与双电控区别
  10. 2020年汽车芯片行业深度报告-1