Android 原生 Picture in Picture 畫中畫功能避坑指南

最近生活有些變動所以斷更好久屿岂,不過雖遲到但永遠(yuǎn)不會缺席践宴。ChatGPT 浪潮還在持續(xù)擴(kuò)大,各位同學(xué)一定要體驗(yàn)體驗(yàn)丫~

這篇主要介紹最近需求中遇到的問題爷怀,希望能幫助后來者少踩坑阻肩。先說結(jié)論:Android 原生畫中畫功能并不完善,如果可以接受 APP 有兩個任務(wù)棧則可以使用运授;否則趁早自己用浮窗自定義實(shí)現(xiàn)畫中畫的功能吧烤惊。

1. PiP 簡介

Android PiP 模式也稱之為畫中畫模式,允許用戶在使用應(yīng)用程序的同時(shí)吁朦,在屏幕的一角或一側(cè)浮動顯示另一個應(yīng)用程序或視頻柒室。這使得用戶可以同時(shí)進(jìn)行多項(xiàng)任務(wù),而不必切換應(yīng)用程序或中斷正在進(jìn)行的任務(wù)逗宜。如下所示:

圖 1 PiP示例

(注:B站的 PiP 是自定義實(shí)現(xiàn)的雄右,未使用系統(tǒng) PiP)

2. 準(zhǔn)備工作,跑通 Demo

官方文檔:https://developer.android.google.cn/guide/topics/ui/picture-in-picture?hl=zh-cn
官方Demo:https://github.com/android/media-samples/tree/main/PictureInPictureKotlin

打開官方 Demo纺讲,首先得改一下 minSdkVersion擂仍,demo 里設(shè)置的是 API 31(Android 12.0),不滿足實(shí)際應(yīng)用需求熬甚,這里改為 23(Android 6.0). 但 PiP 功能只能在 Android8.0 及以上的系統(tǒng)上使用逢渔,所以用到一些方法時(shí),需要注明 @RequiresApi(Build.VERSION_CODES.O)则涯。所以复局,如果需要在 Android 8.0 以下的設(shè)備支持 PiP,只能使用自定義懸浮窗實(shí)現(xiàn)粟判。

還需要注釋掉 setAutoEnterEnabled(true)亿昏、setSeamlessResizeEnabled(false) 這兩個方法。因?yàn)樗鼈冎荒茉?Android 12.0 及以上系統(tǒng)使用档礁,且對于 PiP 的主體功能沒有影響角钩。setAutoEnterEnabled 用于設(shè)置 Activity 在退到后臺時(shí)是否自動進(jìn)入 PiP 模式,當(dāng)設(shè)置為 true呻澜,則在用戶點(diǎn)擊 Home 鍵回到主屏幕時(shí)递礼,Activity 可自動進(jìn)入 PiP 模式,而不用開發(fā)者手動調(diào)用 enterPictureInPictureMode 方法羹幸;setSeamlessResizeEnabled 用于設(shè)置非視頻畫中畫時(shí)的動畫效果脊髓,不影響功能。

按照上述的內(nèi)容設(shè)置完后就可以將 Demo 跑通了栅受。

3. 示例代碼分析

僅分析查看了 Demo 中的 MovieActivity 中的 PiP 相關(guān)的代碼将硝。比較重要的代碼如下:

// code 1
    @RequiresApi(Build.VERSION_CODES.O)
    private fun minimize() {
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

調(diào)用 enterPictureInPictureMode(@NonNull PictureInPictureParams params) 方法就可以進(jìn)入 PiP恭朗,聲明如下:

// code 2
    public boolean enterPictureInPictureMode(@NonNull PictureInPictureParams params) { 
        ···
    }

方法簡介:它是 Activity 類中的方法,需要傳遞一個 PictureInPictureParams 類型對象依疼。當(dāng)系統(tǒng)成功將該 Activity 切換到 PiP 模式或已經(jīng)處于 PiP痰腮,則返回值為 true;如果設(shè)備不支持 PiP 則返回 false律罢。

再來看下構(gòu)建 PictureInPictureParams 類型對象的 updatePictureInPictureParams() 方法:

// code 3
@RequiresApi(Build.VERSION_CODES.O)
    private fun updatePictureInPictureParams(): PictureInPictureParams {
        // 1膀值、計(jì)算出 PiP 小窗的寬高比,這里直接使用播放視頻的控件寬和高計(jì)算
        val aspectRatio = Rational(binding.movie.width, binding.movie.height)
        // 2误辑、將播放視頻的控件binding.movie設(shè)置為 PiP 中要展示的部分
        val visibleRect = Rect()
        binding.movie.getGlobalVisibleRect(visibleRect)
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            // 3沧踏、指定進(jìn)入畫中畫的屏幕部分。系統(tǒng)根據(jù)這個可實(shí)現(xiàn)平滑動畫效果稀余。這里就把之前生成的 visibleRect 傳值過去
            .setSourceRectHint(visibleRect)
            .build()
        setPictureInPictureParams(params)
        return params
    }

updatePictureInPictureParams 方法作用是構(gòu)建出進(jìn)入 PiP 的一些參數(shù)悦冀,比如進(jìn)入小窗的控件趋翻,小窗的寬高比等睛琳。注釋很清楚,源碼直接拿來套用就行踏烙。需要注意的點(diǎn):只能指定 PiP 模式的寬高比师骗,并不能直接設(shè)置寬和高的具體值,系統(tǒng)會根據(jù)設(shè)置的寬高比自己計(jì)算具體值讨惩。

如果在播放器控件上層有其他的操作按鈕等辟癌,還需要在 onPictureInPictureModeChanged 回調(diào)中進(jìn)行處理,即進(jìn)入 PiP 后隱藏這些按鈕荐捻;退出后恢復(fù)這些按鈕的狀態(tài)黍少。 如下是 Demo 中的實(shí)現(xiàn):

// code 4
    override fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean, newConfig: Configuration
    ) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        if (isInPictureInPictureMode) {
            // Hide the controls in picture-in-picture mode.
            binding.movie.hideControls()
        } else {
            // Show the video controls if the video is not playing
            if (!binding.movie.isPlaying) {
                binding.movie.showControls()
            }
        }
    }

通過這個方法可以監(jiān)聽 PiP 的進(jìn)入和退出。

還有一些是 PiP 模式下的播放/暫停处面、上一個/下一個 操作按鈕厂置,即下圖紅框中的這三個按鈕,相關(guān)的使用方式 Demo 中已有示例魂角,這里不再贅述昵济。

圖 1 PiP 按鈕

除此之外,還要在需要進(jìn)入 PiP 的 Activity 的 AndroidManifest 中設(shè)置支持 PiP 的屬性以及處理布局配置更改野揪。這樣一來访忿,如果在 PiP 模式轉(zhuǎn)換期間出現(xiàn)布局更改,該 Activity 就不會重新啟動斯稳。

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

這些官方文檔中就有海铆,這里也不再多說。

4. 功能實(shí)現(xiàn)及踩坑匯總

4.1 實(shí)現(xiàn)點(diǎn)擊 Back 鍵及 Home 鍵自動進(jìn)入 PiP

用戶在觀看視頻時(shí)挣惰,點(diǎn)擊返回鍵或 Home 鍵卧斟,當(dāng)前 Activity 需要進(jìn)入 PiP 繼續(xù)播放系草,這是個常見的功能,實(shí)現(xiàn)起來也比較簡單:

// code 6
    // 實(shí)現(xiàn)點(diǎn)擊返回鍵進(jìn)入 PiP
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onBackPressed() {
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

    // 實(shí)現(xiàn)點(diǎn)擊 Home 鍵進(jìn)入 PiP
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onUserLeaveHint() {
        super.onUserLeaveHint()
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

如果設(shè)置了之前提到的 setAutoEnterEnabled(true) 方法唆涝,則可以不用在 onUserLeaveHint() 回調(diào)里主動調(diào)用 enterPictureInPictureMode 方法進(jìn)入 PiP找都。但建議還是不用 setAutoEnterEnabled,因?yàn)樗荒茉?Android 12 上使用廊酣。能耻。。

onUserLeaveHint() 方法也是 Activity 中的方法亡驰,當(dāng) Activity 進(jìn)入后臺時(shí)就會調(diào)用它晓猛,比如用戶點(diǎn)擊 Home 鍵就會回調(diào)它。但有來電時(shí)凡辱,來電的 Activity 會自動帶到前臺戒职,這時(shí)被退到后臺的 Activity 的 onUserLeaveHint 方法并不會被調(diào)用。onUserLeaveHint 的調(diào)用時(shí)機(jī)是在 onPause 方法之前透乾,這點(diǎn)需要注意洪燥。

4.2 實(shí)現(xiàn) Activity 處于 PiP 時(shí)再次進(jìn)入更新視頻

假設(shè) MovieActivity 已處于 PiP 并正在播放視頻,用戶點(diǎn)擊另外一個視頻又要跳轉(zhuǎn)到 MovieActivity 的情形乳乌。如果不進(jìn)行處理就會出現(xiàn)有兩個 MovieActivity 同時(shí)播放視頻的情況捧韵,即小窗播放的同時(shí),還有一個另一個 MovieActivity 也在播放汉操。如下所示再来,本來只有一個 PiP 在播放視頻,然后點(diǎn)擊 WATCH VIDEO TWO 按鈕又進(jìn)入了 MovieActivity磷瘤,此時(shí)有兩個視頻同時(shí)在播放:

圖 2

查看堆棧信息確實(shí)有兩個 MovieActivity:

圖 3

這種情況下是需要將 MovieActivity 由 PiP 恢復(fù)到正常狀態(tài)并播放新的視頻芒篷,如果視頻內(nèi)容沒有變則接著播放原視頻。官方 Demo 也有說明如何處理采缚,需要兩個步驟:
1)將 MovieActivity 的 launchMode 設(shè)置為 singleTask针炉;
2)在 MovieActivity 的 onNewIntent 方法里處理更新數(shù)據(jù)等邏輯;

比如我在打開 MovieActivity 時(shí)通過 Intent 傳遞不同的 video 來播放不同的視頻仰担,那么在 onNewIntent 中就需要接收傳遞的參數(shù)并更新:

// code 7
// MainActivity.kt    通過 Intent 傳入不同的視頻
        binding.btnWatchVid1.setOnClickListener {
            val intent = Intent(this, MovieActivity::class.java)
            intent.putExtra(MovieActivity.KEY_VIDEO_ID, R.raw.vid_bigbuckbunny)
            startActivity(intent)
        }
        binding.btnWatchVid2.setOnClickListener {
            val intent = Intent(this, MovieActivity::class.java)
            intent.putExtra(MovieActivity.KEY_VIDEO_ID, R.raw.vid_dajiang)
            startActivity(intent)
        }
// code 8
// MovieActivity.kt    onNewIntent 接收并更新
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        val newVideoId = intent?.getIntExtra(KEY_VIDEO_ID, R.raw.vid_bigbuckbunny)
        newVideoId?.let {
            // 更新視頻
            binding.movie.setVideoResourceId(it)
        }
    }

在實(shí)際中可能更加復(fù)雜糊识,但大體思路是一致的。

4.3 實(shí)現(xiàn)跳轉(zhuǎn)其他 Activity 時(shí)摔蓝,當(dāng)前 Activity 自動進(jìn)入 PiP

場景:正在 MovieActivity 里播放視頻赂苗,用戶點(diǎn)擊某個按鈕跳轉(zhuǎn)到其他 Activity,MovieActivity 此時(shí)需進(jìn)入 PiP贮尉,用戶可以在新打開的 Activity 頁面進(jìn)行操作拌滋。

官方文檔里并沒有針對這一場景進(jìn)行說明和提示,所以一開始以為很簡單猜谚,直接跟之前一樣調(diào)用 enterPictureInPictureMode(updatePictureInPictureParams()) 不就可以了么败砂?于是就有了下面的代碼:

// code 9
        binding.btnJumpTestOne.setOnClickListener {
            enterPictureInPictureMode(updatePictureInPictureParams())
            startActivity(Intent(this@MovieActivity, TestOneActivity::class.java))
        }

想著先將當(dāng)前的 MovieActivity 進(jìn)入 PiP赌渣,再跳轉(zhuǎn)到其他的 Activity,結(jié)果 MovieActivity 直接退出了昌犹,也沒有錯誤信息坚芜。看棧信息發(fā)現(xiàn)其實(shí)要跳轉(zhuǎn)的新 Activity —— TestOneActivity 已經(jīng)打開了斜姥。鸿竖。。

打開失敗的動圖:

圖 4 跳轉(zhuǎn)失敗示例1

在 MovieActivity 中點(diǎn)擊 JUMP TO TESTONEACTIVITY 按鈕跳轉(zhuǎn)之后铸敏,堆棧信息如下缚忧,可以看到 pid = 21126 的進(jìn)程就是 Demo 程序,TestOneActivity 確實(shí)打開了杈笔,MovieActivity 已退出:

圖 5

加延時(shí)再試:

// code 10
        binding.btnJumpTestOne.setOnClickListener {
            lifecycleScope.launch {
                enterPictureInPictureMode(updatePictureInPictureParams())
                delay(1000)
                startActivity(Intent(this@MovieActivity, TestOneActivity::class.java))
            }
        }

確實(shí)進(jìn)入 PiP 了闪水,但后面跳轉(zhuǎn)的 TestOneActivity 也在 PiP 了。蒙具。

圖 6 跳轉(zhuǎn)失敗示例2

如果是先跳轉(zhuǎn) TestOneActivity 再進(jìn)入 PiP 球榆,經(jīng)測試只會跳轉(zhuǎn)并不會進(jìn)入 PiP,這里就不再展示了店量。

經(jīng)分析和實(shí)踐發(fā)現(xiàn)芜果,只能先進(jìn)入 PiP 再進(jìn)行跳轉(zhuǎn),之所以會出現(xiàn)在 PiP 里跳轉(zhuǎn)融师,是因?yàn)楹竺嫣D(zhuǎn)的 TestOneActivity 進(jìn)入了 MovieActivity 所在的任務(wù)棧。Activity 在沒有設(shè)置 taskAffinity 屬性時(shí)蚁吝,都會放在默認(rèn)的同一個任務(wù)棧中旱爆。

所以想到的第一個方法就是,修改 MovieActivity 的 launchMode窘茁,改為 singleInstance怀伦。這樣既可以保證任務(wù)棧中只有一個 MovieActivity 的實(shí)例,也可以將 MovieActivity 放在獨(dú)立的任務(wù)棧中山林。試了下果然可以了房待,但會在多任務(wù)切換頁里出現(xiàn)同一個 App 有兩個任務(wù)棧的現(xiàn)象:

圖7 一個 App 出現(xiàn)多個任務(wù)棧

這是第一個問題,這個問題直到最后也無法解決驼抹,在 AndroidManifest 文件中添加 autoRemoveFromRecentsexcludeFromRecents 都沒用桑孩,還是會在多任務(wù)切換頁出現(xiàn)兩個棧。

還有一個問題即問題二框冀,還是 singleInstance 引起的流椒。當(dāng) MovieActivity 正在以非小窗模式播放視頻時(shí),先進(jìn)入多任務(wù)切換頁明也,再按 Home 鍵回到主屏幕宣虾,然后再點(diǎn)擊 App 圖標(biāo)進(jìn)入時(shí)惯裕,發(fā)現(xiàn)進(jìn)入的不是 MovieActivity,而是 MovieActivity 的上個頁面绣硝,即 MainActivity蜻势,此時(shí)再進(jìn)入多任務(wù)切換頁面,會發(fā)現(xiàn) MovieActivity 所在那個任務(wù)棧已經(jīng)消失了鹉胖。這里其實(shí)有兩個問題:
1)回到主屏幕后再點(diǎn)擊 App 圖標(biāo)應(yīng)該回到 MovieActivity咙边;
2)用戶并沒有關(guān)閉 MovieActivity,但進(jìn)入多任務(wù)切換頁面后無法找到 MovieActivity 了次员。如下動圖:

圖 8 問題二

問題二的兩個問題得先解決 2)才能解決 1)败许。2)之所以會出現(xiàn)是因?yàn)橐粋€ App 出現(xiàn)了兩個任務(wù)棧,這兩個任務(wù)棧的 taskAffinity參數(shù)默認(rèn)是一樣的淑蔚,一山不容二虎市殷,那么點(diǎn)擊桌面圖標(biāo)后,就會把之前的任務(wù)棧移到前臺刹衫,然后會把另一個任務(wù)棧干掉醋寝。
所以首先要保留這兩個任務(wù)棧,給 MovieActivity 設(shè)置一個單獨(dú)的 taskAffinity名稱带迟,這就可以得以保留音羞,問題 2)就解決了。只有先保留任務(wù)棧仓犬,才能解決問題 1)嗅绰。

導(dǎo)致問題1)的原因是因?yàn)橛脩粼邳c(diǎn)擊 App 圖標(biāo)時(shí),會將 MainActivity 所在的棧移到前臺搀继,那么首先可以想到的方法是窘面,在點(diǎn)擊 App 圖標(biāo)時(shí),將含有 MovieActivity 的棧移到前臺顯示叽躯。所以我們可以注冊一個生命周期監(jiān)聽财边,在 onResume 時(shí),去遍歷 App 的所有任務(wù)棧点骑,找到含有 MovieActivity 的棧并將其移到前臺即可:

// code 11   DemoApplication.kt  onCreate方法中
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks{
    ···
    override fun onActivityResumed(activity: Activity) {
        val appCompatActivity = if (activity is AppCompatActivity) {
            activity
        } else {
            return
        }
        // 限制條件:所有的 activity 必須為 AppCompatActivity 或其子類
        val activityManager = appCompatActivity
            .getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        for (i in activityManager.appTasks.indices) {
            val appTask = activityManager.appTasks[i]
            val taskInfo = appTask.taskInfo
            if (taskInfo.topActivity == null) continue
            val topActivityName = taskInfo.topActivity?.className
            if ((!topActivityName.isNullOrBlank() && topActivityName.contains(
                    MovieActivity::class.java.simpleName
                )) && i != 0
            ) {
                // 如果存在視頻播放頁且所在的 Task 不在前臺酣难,則需要將其移到前臺
                activityManager.moveTaskToFront(
                    taskInfo.id,
                    ActivityManager.MOVE_TASK_NO_USER_ACTION
                )
            }
        }
    }
    ···
})

很明顯這個方法并不好,App 中每個 Activity 在調(diào)用 onResume 時(shí)都會走一遍這個邏輯黑滴;且 App 中所有的 Activity 必須為 AppCompatActivity 或它的子類憨募。還得需要申請 REORDER_TASKS 權(quán)限:

// code 12   AndroidManifest.xml
<!--  申請可排序任務(wù)棧權(quán)限  -->
<uses-permission android:name="android.permission.REORDER_TASKS" />

并且這里還遇到一個問題:當(dāng)在 MovieActivity 跳轉(zhuǎn)到 TestOneActivity 時(shí),進(jìn)入 PiP跷跪,此時(shí)點(diǎn)擊 PiP 中的關(guān)閉按鈕關(guān)閉 PiP馋嗜,然后點(diǎn)擊 Home 回到桌面,再點(diǎn)擊 App 圖標(biāo)會發(fā)現(xiàn)進(jìn)入的是 MovieActivity 頁吵瞻,而并不是 TestOneActivity:

圖 9 點(diǎn)擊App進(jìn)入頁面不對的問題

經(jīng)分析葛菇,原因是 PiP 的關(guān)閉按鈕點(diǎn)擊后甘磨,只是將 MovieActivity 退到了后臺,并沒有銷毀眯停。济舆。。所以退到后臺莺债,再點(diǎn)擊 App 圖標(biāo)時(shí)滋觉,會將包含 MovieActivity 的任務(wù)棧顯示到前臺,而不顯示 TestOneActivity 所在的任務(wù)棧齐邦。那么我們就需要在關(guān)閉 PiP 按鈕的回調(diào)中直接關(guān)閉 Activity椎侠,但我們開發(fā)者拿不到關(guān)閉按鈕的回調(diào),所以就有了下面的問題:

如何在用戶點(diǎn)擊 PiP 里的關(guān)閉按鈕時(shí)措拇,關(guān)閉 PiP 所在的 Activity我纪?
經(jīng)多次實(shí)驗(yàn)得知,PiP 雖然沒有關(guān)閉小窗的回調(diào)丐吓,但會先調(diào)用 onStop 然后會調(diào)用 onPictureInPictureModeChanged 方法浅悉。所以可以根據(jù)是否回調(diào)了 onStop 來間接判斷是否點(diǎn)擊了 PiP 小窗里的關(guān)閉按鈕。

// code 13
// MovieActivity 的 ViewModel
class MovieViewModel: ViewModel() {
    //進(jìn)入或退出畫中畫模式所在Activity的事件 true: 進(jìn)入券犁; false: 退出
    val enterOrExitPiPMode = MutableLiveData<Boolean>()
}

// MovieActivity.kt
@RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(
    isInPictureInPictureMode: Boolean, newConfig: Configuration
) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
    if (isInPictureInPictureMode) {
        ···
    } else {
        ···
        // PiP沒有關(guān)閉小窗的回調(diào)术健,但會先回調(diào) onStop 然后回調(diào) onPictureInPictureModeChanged 方法≌吵模可以根據(jù)
        // 是否回調(diào)了 onStop 來間接判斷是否點(diǎn)擊了PiP小窗里的關(guān)閉按鈕荞估,這里需要在用戶主動關(guān)閉小窗后 finish 掉
        // MovieActivity
        if (lifecycle.currentState < Lifecycle.State.STARTED) {
            movieViewModel.enterOrExitPiPMode.value = false
        }
    }
}

// MovieActivity.kt
// 進(jìn)入 or 退出畫中畫模式
movieViewModel.enterOrExitPiPMode.observe(this) {
    if (it) {
        // 這里暫沒有操作
    } else {
        // 關(guān)閉畫中畫模式事件,需要直接 finish 掉 MovieActivity
        finish()
    }
}

這里使用 LiveData 是因?yàn)樵谄渌捻撁婵赡芤残枰P(guān)閉 PiP色难,所以可以先獲得 ViewModel泼舱,通過更新 enterOrExitPiPMode 的值去關(guān)閉 PiP。

4.4 去掉 PiP 下方自帶的三個按鈕

PiP 小窗上的 6個按鈕只有底部的三個按鈕可自定義枷莉,另外的三個按鈕無法修改,這也是為什么無法拿到關(guān)閉按鈕回調(diào)的原因尺迂。如果需要針對底部的三個按鈕進(jìn)行自定義笤妙,通過設(shè)置 PictureInPictureParams 參數(shù)實(shí)現(xiàn),但最多只能自定義 3個噪裕,我們這里不需要這三個按鈕蹲盘,就可以設(shè)置一個透明按鈕間接去掉:

// code 14
// 第一步:新建一個 RemoteAction list
@RequiresApi(Build.VERSION_CODES.O)
private fun initPiPActions(): List<RemoteAction> {
    //去掉原生小窗中默認(rèn)自帶的 上一個、暫停膳音、下一個 三個按鈕
    val actions = mutableListOf<RemoteAction>()
    val emptyIntent = PendingIntent.getBroadcast(requireContext(), 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
    actions.add(RemoteAction(Icon.createWithResource(requireContext(), R.drawable.divider_transparent), "", "", emptyIntent))
    return actions
}

// 第二步:設(shè)置到 PictureInPictureParams 參數(shù)中
val params = PictureInPictureParams.Builder()
    .setAspectRatio(aspectRatio)
    // Specify the portion of the screen that turns into the picture-in-picture mode.
    // This makes the transition animation smoother.
    .setSourceRectHint(visibleRect)
    .setActions(initPiPActions())
    .build()

自定義底部三個按鈕的方法有兩種:一是通過實(shí)現(xiàn) RemoteAction 的方法召衔;二是官方 Demo 中的方法。關(guān)于這個內(nèi)容參考文獻(xiàn)2 更加詳實(shí)祭陷,可以借鑒苍凛。

5. 難以解決的問題

以上的坑基本趟完了趣席,但下面的坑實(shí)在是難以解決,這里也歡迎大佬們能給出建議醇蝴。

5.1 App 出現(xiàn)兩個任務(wù)棧

為了實(shí)現(xiàn)從 MovieActivity 跳轉(zhuǎn)到其他 Activity 時(shí)宣肚,MovieActivity 自身進(jìn)入 PiP,必須將 MovieActivity 放到獨(dú)立的任務(wù)棧中悠栓,所以就會出現(xiàn)這個問題霉涨。以上文中也有說明。

5.2 PiP 模式下跳轉(zhuǎn)一個 singleTask 的 Activity 會在 PiP 中跳轉(zhuǎn)

官方 Demo 中將 MovieActivity 的 launchMode 設(shè)置為 singleTask 且不設(shè)置 taskAffinity 時(shí)惭适,當(dāng) MovieActivity 正處于 PiP 模式下笙瑟,跳轉(zhuǎn)到另一個 Activity 時(shí),目標(biāo) Activity 的 launchMode 不能為 singleTask癞志,否則目標(biāo) Activity 會在 PiP 中跳轉(zhuǎn)往枷。

圖10

這里將 TestOneActivity 的 launchMode 設(shè)置為 singleTask,然后從 MovieActivity 跳到 TestOneActivity 時(shí)今阳,TestOneActivity 出現(xiàn)在了 PiP 中师溅。而通常項(xiàng)目中會有許多 Activity 的 launchMode 設(shè)置為了 singleTask,所以原生 PiP 方案最終被否盾舌。墓臭。。
github 上也有 issue:https://github.com/android/media-samples/issues/85

6. 小知識點(diǎn)匯總

6.1 ActivityManager.MOVE_TASK_NO_USER_ACTION 的作用

常用于 activityManager.moveTaskToFront 方法中妖谴,意思是不把當(dāng)前的操作看作是用戶觸發(fā)的行為窿锉,即不會調(diào)用當(dāng)前 Activity 的 onUserLeaveHint 方法。還有一個是 ActivityManager.MOVE_TASK_WITH_HOME 膝舅,這個就會調(diào)用當(dāng)前 Activity 的 onUserLeaveHint 方法嗡载。實(shí)際應(yīng)用中貌似很少用到。

6.2 autoRemoveFromRecents 和 excludeFromRecents 的用法

6.2.1 android:autoRemoveFromRecents 用法

android:autoRemoveFromRecents 是在任務(wù)棧中的最后一個 Activity 完成之前仍稀,由具有此屬性的 Activity 啟動的任務(wù)棧是否保留在多任務(wù)切換頁面中洼滚。即 autoRemoveFromRecents 指定了當(dāng) Activity 被系統(tǒng)回收時(shí),是否保留在多任務(wù)切換頁面中技潘。默認(rèn)值為 false遥巴。

當(dāng)設(shè)置為 true 時(shí): 當(dāng) Activity 被系統(tǒng)回收時(shí),從最近使用的多任務(wù)切換頁中移除該 Activity 所在的任務(wù)棧享幽;當(dāng)設(shè)置為 false 時(shí): 當(dāng) Activity 被系統(tǒng)回收時(shí)铲掐,不從最近使用的多任務(wù)切換頁中移除該 Activity 所在的任務(wù)棧。

這個屬性主要用于:
1)一些臨時(shí) Activity值桩,當(dāng)它們被銷毀后摆霉,不希望它們出現(xiàn)在多任務(wù)切換頁中,可以設(shè)置為 true;
2)一些沒有重要數(shù)據(jù)的 Activity携栋,如果設(shè)置為 true搭盾,當(dāng)內(nèi)存不足被系統(tǒng)回收后,由于它已經(jīng)從多任務(wù)切換頁移除刻两,用戶不太可能再去恢復(fù)它增蹭,及時(shí)移除有利于內(nèi)存回收;
3)一些包含敏感數(shù)據(jù)的 Activity磅摹,為了安全考慮滋迈,不希望它出現(xiàn)在多任務(wù)切換頁中,可以設(shè)置為 true。
所以户誓,總體來說饼灿,這個屬性主要是出于內(nèi)存管理和安全考慮,控制 Activity 在被系統(tǒng)回收后是否從多任務(wù)切換頁中移除帝美。

6.2.2 android:excludeFromRecents 用法

android:excludeFromRecents 也是一個 Activity 屬性碍彭,它指定了是否從多任務(wù)切換頁中排除該 Activity 所在的任務(wù)棧。默認(rèn)值為 false悼潭。

當(dāng)設(shè)置為 true 時(shí):該 Activity 所在的任務(wù)棧不會出現(xiàn)在多任務(wù)切換頁中庇忌;當(dāng)設(shè)置為 false 時(shí):不從多任務(wù)切換頁中排除該 Activity 所在的任務(wù)棧。

這個屬性與 android:autoRemoveFromRecents 很像舰褪,它們的區(qū)別是:
android:autoRemoveFromRecents 是當(dāng) Activity 被系統(tǒng)回收時(shí)皆疹,所在棧是否從多任務(wù)切換頁中移除;
android:excludeFromRecents 是 Activity 所在的棧從一開始就不會出現(xiàn)在任務(wù)列表中占拍。

更多內(nèi)容略就,歡迎關(guān)注公眾號:修之竹
或者查看 修之竹的 Android 專輯

贊人玫瑰,手留余香晃酒!歡迎點(diǎn)贊表牢、轉(zhuǎn)發(fā)~ 轉(zhuǎn)發(fā)請注明出處~

參考文獻(xiàn)

  1. 官方文檔:https://developer.android.google.cn/guide/topics/ui/picture-in-picture?hl=zh-cn
  2. 總結(jié)系列-Android畫中畫模式-看這篇就夠啦; ZhangQiang-; https://blog.csdn.net/u011200604/article/details/104701266
  3. Android 面試黑洞——當(dāng)我按下 Home 鍵再切回來,會發(fā)生什么贝次?; 扔物線朱凱崔兴; https://www.bilibili.com/video/BV1CA41177Se/
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蛔翅,隨后出現(xiàn)的幾起案子恼布,更是在濱河造成了極大的恐慌,老刑警劉巖搁宾,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異倔幼,居然都是意外死亡盖腿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翩腐,“玉大人鸟款,你說我怎么就攤上這事∶裕” “怎么了何什?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長等龙。 經(jīng)常有香客問我处渣,道長,這世上最難降的妖魔是什么蛛砰? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任罐栈,我火速辦了婚禮,結(jié)果婚禮上泥畅,老公的妹妹穿的比我還像新娘。我一直安慰自己位仁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布轿亮。 她就那樣靜靜地躺著按咒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掠抬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天劣坊,我揣著相機(jī)與錄音,去河邊找鬼帮寻。 笑死藕帜,一個胖子當(dāng)著我的面吹牛贝攒,可吹牛的內(nèi)容都是我干的荒适。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼糠馆,長吁一口氣:“原來是場噩夢啊……” “哼盛霎!你這毒婦竟也來了掉奄?” 一聲冷哼從身側(cè)響起缤苫,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤镀钓,失蹤者是張志新(化名)和其女友劉穎窟赏,沒想到半個月后规哪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝠嘉,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年杯巨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蚤告。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡服爷,死狀恐怖杜恰,靈堂內(nèi)的尸體忽然破棺而出获诈,到底是詐尸還是另有隱情,我是刑警寧澤心褐,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布舔涎,位于F島的核電站,受9級特大地震影響逗爹,放射性物質(zhì)發(fā)生泄漏亡嫌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一掘而、第九天 我趴在偏房一處隱蔽的房頂上張望挟冠。 院中可真熱鬧,春花似錦袍睡、人聲如沸知染。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽控淡。三九已至,卻和暖如春伪窖,著一層夾襖步出監(jiān)牢的瞬間逸寓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工覆山, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留竹伸,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓簇宽,卻偏偏與公主長得像勋篓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子魏割,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容