動手做個PIP APP吧

Jia-Ming 2020/12/08 23:38:34
3378

大部分的人都用過YT或視訊通話的APP,那聽過PIP?

 

PIP全名Picture-in-picture(子母畫面,也有人說是畫中畫),是android8.0後提供的多視窗功能,可以將APP的畫面縮小到螢幕的一小角,而且支援手指拖曳的動作,可以將APP移動到螢幕的任意地方(但不能超過螢幕邊界).進入PIP模式後,就可以實現同時觀看影片又聊天,或者是在視訊通話時一邊查看行事曆了.

 

看到這裡你應該知道PIP是什麼了,或許你很常使用 但是不知道他就是PIP

 

那麼接下來做一個自己的PIP APP吧.首先要在manifest中告訴系統我們要使用這個功能android:supportsPictureInPicture="true",因為預設的情況下是關閉的,另外還要給幾個設定,讓APP進入PIP模式後能後夠正常顯示畫面.

<application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:resizeableActivity="true"
            android:supportsPictureInPicture="true"
            android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

 

 

如果忘了設定支援PIP的話就會看到這個訊息:

 

設定完成之後接著就是要給個開關讓APP進入PIP了,可以用個按鈕或是另外做個事件去觸發.

用個簡單的例子吧,先做個layout把按鈕擺上去

 

public class MainActivity extends AppCompatActivity {

    private Button btnPIP;
    
    @Override

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        btnPIP = findViewById(R.id.btnPIP);
        btnPIP.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                enterPictureInPictureMode();
            }
        });
    }
}

 

 

你沒看錯,就這麼一行就可以進入PIP模式了,但是坑還沒開始踩呢.不是每隻手機都能用PIP的,先確認開不開得起來吧.

加上這行hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)判斷手機是否能夠使用PIP,如果不能使用PIP也提示一下使用者.

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        btnPIP = findViewById(R.id.btnPIP);
        btnPIP.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)){
                    Toast.makeText(
                            MainActivity.this,
                            "Device not support PIP mode !",
                            Toast.LENGTH_SHORT).show();
                }else {
                    enterPictureInPictureMode();
                }
            }
        });
    }

 

按下按鈕試試看(左邊是按下按鈕後的樣子,右邊是點擊已經縮小的畫面後聚焦的樣子,會微微的放大)

 

 

 

如果想要在使用者點擊Home鍵時主動幫使用者進入PIP模式,可以利用onUserLeaveHint() 方法,判斷當使用者按下Home鍵時的情境.

我們可以透過isInPictureInPictureMode()方法判斷目前是否在PIP模式下.

@Override
    protected void onUserLeaveHint() {
        super.onUserLeaveHint();

        if(!isInPictureInPictureMode()){
            enterPictureInPictureMode();
        }
    }

 

 

使用PIP的時候要注意一下layout的擺放,看到上面放的圖示應該就能明白,原本Layout上大部分的元件在進入PIP模式後是看不見的(如果使用ConstraintLayout把元件寬高都改成0dp,利用拘束去比例縮放的話倒是可以看到縮小後的畫面),另外中間的按鈕是因為我用拘束把它置中了,才會顯示在那裡,底下的圖例會改掉.

 

在進入PIP模式後,原本Activity上的元件互動是無效的,比如Edittext這個時候是點不到也無法輸入文字的,但是可以擺上幾個簡易的按鈕去處發事件.

 

 

進入PIP模式後的小視窗寬高比例是可以手動改變的,透過Rational設定寬高比,再使用PictureInPictureParams.Builder

private PictureInPictureParams.Builder pipBuilder =
            new PictureInPictureParams.Builder();

    private void setPipBuilder(){
        Rational rational = new Rational(9,21);
        pipBuilder.setAspectRatio(rational);
    }
    
    private void intoPIPMode(){
        enterPictureInPictureMode(pipBuilder.build());
    }​

 

接著放個影片到VideoView上跑跑看.

private void initVideoView(){
        //連結引用自android官方的PIP範例影片
        Uri uri = Uri.parse("https://developer.android.com/images/pip.mp4");
        videoView.setVideoURI(uri);
        videoView.start();

        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                Log.d(TAG,"video onPrepared !");
                mp.start();
            }
        });

        videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                Log.d(TAG,"video onCompletion !");
            }
        });

        videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                Log.d(TAG,"video onError !");
                return false;
            }
        });
    }

 

 

我們可以觀察一下進入PIP模式時的生命週期變化:

@Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
                                              Configuration newConfig) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
        Log.d(TAG,"onPIPMode");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG,"onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG,"onResume");
    }

 

發現是在onPasue之後進入PIP模式,而且不會進到omStop.

解除PIP模式恢復一般大小時也不會觸發onStart,那麼影片應概會在變更模式後繼續播放.

 

接下來開始製作影片播放以及停止的按鈕吧,先定義等等會用到的參數.

private static final String VIDEO_ACTION_CONTROL = "ACTION_VIDEO_CONTROL";

    private static final String VIDEO_STATUS = "VIDEO_STATUS";

    private static final int VIDEO_STATUS_PLAY = 1;
    private static final int VIDEO_STATUS_PAUSE = 2;

    private static final int REQUEST_CODE_PLAY = 1;
    private static final int REQUEST_CODE_PAUSE = 2;

    private static final String PLAY = "PLAY";
    private static final String PAUSE = "PAUSE";

 

透過PendingIntentRemoteAction來進行操作.

private void updatePIPActions(@DrawableRes int iconResId, String title,
                                  int controlType, int requestCode){
        final ArrayList<RemoteAction> actions = new ArrayList<>();

        final PendingIntent intent =
                PendingIntent.getBroadcast(
                        MainActivity.this,
                        requestCode,
                        new Intent(VIDEO_ACTION_CONTROL).putExtra(VIDEO_STATUS, controlType),
                        0);
        final Icon icon = Icon.createWithResource(MainActivity.this, iconResId);
        actions.add(new RemoteAction(icon, title, title, intent));

        pipBuilder.setActions(actions);
        setPictureInPictureParams(pipBuilder.build());
    }

這裡可以看到我是用ArrayList來加上按鈕的action的,但是能夠放上去的按鈕數量是有限制的,透過getMaxNumPictureInPictureActions()這個方法可以得到3得回傳值,做多就放三個按鈕.

 

前面提到在PIP模式下,原本layout上的元件不會獲得互動事件,我們透過發送接收廣播來告訴系統要做什麼動作.

因為是進入PIP模式後才發生的,我就直接寫在onPictureInPictureModeChanged方法裡.

除了廣播之外也同時處理了一下View的顯示,畢竟layout上大部分的元件都看不到了乾脆隱藏掉!讓影片多一點空間.

@Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
                                              Configuration newConfig) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
        Log.d(TAG,"onPIPMode");

        if (isInPictureInPictureMode) {
            //隱藏其他的View
            topArea.setVisibility(View.GONE);
            bottomArea.setVisibility(View.GONE);
            btnPIP.setVisibility(View.GONE);

            //動態改變videoView的拘束條件讓他可以滿屏
            ConstraintSet set = new ConstraintSet();
            set.clone(mainLayout);
            set.connect(videoView.getId(),ConstraintSet.BOTTOM,mainLayout.getId(),ConstraintSet.BOTTOM);
            set.applyTo(mainLayout);

            //設定接收廣播
            actionReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if(intent == null || !intent.getAction().equals(VIDEO_ACTION_CONTROL))
                        return;

                    int currentStatus = intent.getIntExtra(VIDEO_STATUS,0);
                    if(currentStatus == VIDEO_STATUS_PLAY){
                        //影片播放的時候可以按下的按鈕是暫停
                        videoView.start();

                        updatePIPActions(
                                R.drawable.ic_baseline_pause_24,
                                PAUSE,
                                VIDEO_STATUS_PAUSE,
                                REQUEST_CODE_PAUSE);

                    }else if(currentStatus == VIDEO_STATUS_PAUSE){
                        //影片暫停的時候可以按下的按鈕是播放
                        videoView.pause();

                        updatePIPActions(
                                R.drawable.ic_baseline_play_arrow_24,
                                PLAY,
                                VIDEO_STATUS_PLAY,
                                REQUEST_CODE_PLAY);
                    }
                }
            };

            registerReceiver(actionReceiver, new IntentFilter(VIDEO_ACTION_CONTROL));

        } else {
            //顯示其他的View
            topArea.setVisibility(View.VISIBLE);
            bottomArea.setVisibility(View.VISIBLE);
            btnPIP.setVisibility(View.VISIBLE);

            //動態恢復videoView的拘束條件
            ConstraintSet set = new ConstraintSet();
            set.clone(mainLayout);
            set.connect(videoView.getId(),ConstraintSet.BOTTOM,topArea.getId(),ConstraintSet.BOTTOM);
            set.applyTo(mainLayout);

            //移除廣播
            unregisterReceiver(actionReceiver);
            actionReceiver = null;
        }
    }

 

記得一開始影片播放的時候也要把action加上去

//連結引用自android官方的PIP範例影片
        Uri uri = Uri.parse("https://developer.android.com/images/pip.mp4");
        videoView.setVideoURI(uri);
        videoView.start();

        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                Log.d(TAG,"video onPrepared !");
                mp.setLooping(true);
                mp.start();
                
                updatePIPActions(
                        R.drawable.ic_baseline_pause_24,
                        PAUSE,
                        VIDEO_STATUS_PAUSE,
                        REQUEST_CODE_PAUSE);
            }
        });

 

      

 

到這邊就完成摟,要做成播放清單的話還需要再加上下一首跟上一首的按鈕吧,最多只能擺放三個按鈕的限制看來就是為這個做的.
那麼後面補個layout吧,這是為了研究縮小後的擺放效果做的,就別計較好不好了看啦.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:id="@+id/mainLayout">

    <View
        android:id="@+id/topArea"
        android:background="#0000ee"
        android:layout_width="0dp"
        android:layout_height="400dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="9:16"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/bottomArea"/>

    <View
        android:id="@+id/bottomArea"
        android:background="#00ee00"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/topArea"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <Button
        android:id="@+id/btnPIP"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello PIP"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/bottomArea" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

補充:

如果開啟第二個APP並且進入PIP模式會發生什麼事呢?

答案:"前一個進入PIP模式的APP會被退出PIP模式"

 

 

參考

https://developer.android.com/guide/topics/ui/picture-in-picture#java
註:截圖中出現的影片引用自官方介紹,連結同上

 

Jia-Ming