הוספת סרטונים באמצעות 'תמונה בתוך תמונה' (PiP)

החל מ-Android 8.0 (רמת API 26), מערכת Android מאפשרת השקה של פעילויות מצב 'תמונה בתוך תמונה' (PiP). 'תמונה בתוך תמונה' הוא סוג מיוחד של מצב 'ריבוי חלונות' בעיקר משמש להפעלת סרטון. היא מאפשרת למשתמש לצפות בסרטון בחלון קטן מוצמד בפינת המסך בזמן ניווט בין אפליקציות או גלישה בתוכן מופעל במסך הראשי.

התכונה 'תמונה בתוך תמונה' משתמשת בממשקי ה-API עם ריבוי חלונות שזמינים ב-Android 7.0 כדי לספק חלון שכבת-על של סרטון מוצמד. כדי להוסיף את 'תמונה בתוך תמונה' לאפליקציה, צריך לרשום את פעילויות שתומכות ב'תמונה בתוך תמונה', להעביר את הפעילות למצב 'תמונה בתוך תמונה' לפי הצורך, וגם לוודא שרכיבי ממשק המשתמש מוסתרים ושהפעלת הסרטון נמשכת כשהפעילות מתבצעת במצב 'תמונה בתוך תמונה'.

חלון 'תמונה בתוך תמונה' מופיע בשכבה העליונה של המסך, בפינה שנבחרה על ידי במערכת.

התכונה 'תמונה בתוך תמונה' נתמכת גם במכשירים תואמים עם מערכת ההפעלה Android TV שפועלת בהם Android 14 (רמת API 34) ואילך. יש הרבה נקודות דמיון, אבל שיקולים נוספים בזמן השימוש PiP בטלוויזיה.

איך המשתמשים יכולים לקיים אינטראקציה עם החלון של 'תמונה בתוך תמונה'

המשתמשים יכולים לגרור את החלון של 'תמונה בתוך תמונה' למיקום אחר. החל מ-Android 12, משתמשים יכול גם:

  • הקישו פעם אחת על החלון כדי להציג מתג במסך מלא, לחצן סגירה, לחצן ההגדרות ופעולות מותאמות אישית שסופקו על ידי האפליקציה שלך (כמו הפעלה ).

  • מקישים הקשה כפולה על החלון כדי להחליף בין הגודל הנוכחי של 'תמונה בתוך תמונה' לבין הגודל המקסימלי או הגודל המינימלי של 'תמונה בתוך תמונה' - לדוגמה, הקשה כפולה על חלון מוגדל מפחיתה אותה, וגם ההפך נכון.

  • כדי להסתיר את החלון, גוררים אותו לקצה השמאלי או הימני. כדי לבטל את ה- החלון, מקישים על החלק הגלוי של החלון השמור או גוררים אותו החוצה.

  • אפשר לשנות את הגודל של חלון 'תמונה בתוך תמונה' באמצעות תנועת צביטה לשינוי מרחק התצוגה.

האפליקציה שלך קובעת מתי הפעילות הנוכחית תעבור למצב 'תמונה בתוך תמונה'. הנה כמה דוגמאות דוגמאות:

  • פעילות מסוימת יכולה לעבור למצב 'תמונה בתוך תמונה' כשהמשתמש מקיש על הלחצן הראשי או מחליק במסך עד הבית. כך יוצגו המסלול במפות Google המשתמש מריץ פעילות נוספת באותו הזמן.

  • האפליקציה שלך יכולה להעביר סרטון למצב 'תמונה בתוך תמונה' כשהמשתמש מנווט חזרה מהמיקום בסרטון כדי לעיין בתוכן אחר.

  • האפליקציה שלך יכולה להעביר סרטון למצב 'תמונה בתוך תמונה' בזמן שמשתמש צופה בסוף פרק תוכן. במסך הראשי מוצגים קידומי מכירות או תקצירים מידע על הפרק הבא בסדרה.

  • האפליקציה יכולה לאפשר למשתמשים להוסיף תוכן נוסף לרשימת 'הבאים בתור' בזמן הם צופים בסרטון. הסרטון ממשיך לפעול במצב 'תמונה בתוך תמונה' בזמן הראשי במסך מוצגת פעילות של בחירת תוכן.

הצהרה על תמיכה ב'תמונה בתוך תמונה'

כברירת מחדל, המערכת לא תומכת באופן אוטומטי ב'תמונה בתוך תמונה' באפליקציות. אם רוצים לתמוך ב'תמונה בתוך תמונה' באפליקציה, לרשום את הפעילות שלך עם הסרטון במניפסט על ידי מגדיר את android:supportsPictureInPicture ל-true. כמו כן, עליכם לציין הפעילות מטפלת בשינויים בתצורת הפריסה, כך שהפעילות הפעלה מחדש כשחלים שינויים בפריסה במהלך מעברים במצב 'תמונה בתוך תמונה'.

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

שינוי הפעילות ל'תמונה בתוך תמונה'

החל מ-Android 12, אפשר להעביר את הפעילות למצב 'תמונה בתוך תמונה' באמצעות הגדרת הדגל setAutoEnterEnabled ל-true. באמצעות ההגדרה הזו, פעילות עובר אוטומטית למצב 'תמונה בתוך תמונה' לפי הצורך, בלי להתקשר במפורש enterPictureInPictureMode() ב-onUserLeaveHint. והפעולה הזאת כוללת היתרון של המעברים הוא הרבה יותר חלק. למידע נוסף, ראו לעשות מעבר למצב 'תמונה בתוך תמונה' בצורה חלקה יותר בניווט באמצעות תנועות.

אם אתם מטרגטים ל-Android מגרסה 11 ומטה, הפעילות חייבת להתקשר enterPictureInPictureMode() כדי לעבור למצב 'תמונה בתוך תמונה'. לדוגמה, הקוד הבא מעביר פעילות אל מצב 'תמונה בתוך תמונה' כשהמשתמש לוחץ על לחצן ייעודי בממשק המשתמש של האפליקציה:

Kotlin

override fun onActionClicked(action: Action) {
    if (action.id.toInt() == R.id.lb_control_picture_in_picture) {
        activity?.enterPictureInPictureMode()
        return
    }
}

Java

@Override
public void onActionClicked(Action action) {
    if (action.getId() == R.id.lb_control_picture_in_picture) {
        getActivity().enterPictureInPictureMode();
        return;
    }
    ...
}

כדאי גם לכלול לוגיקה שמעבירה פעילות למצב 'תמונה בתוך תמונה' במקום זאת להיכנס לרקע. לדוגמה, מפות Google יעבור למצב 'תמונה בתוך תמונה' אם המשתמש לוחץ על הלחצן הראשי או על הלחצן האחרון בזמן שהאפליקציה מנווטת. אפשר כדי לזהות את המקרה הזה, onUserLeaveHint():

Kotlin

override fun onUserLeaveHint() {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode()
    }
}

Java

@Override
public void onUserLeaveHint () {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode();
    }
}

מומלץ: לספק למשתמשים חוויית מעבר מטופחת ב'תמונה בתוך תמונה'

ב-Android 12 נוספו שיפורים קוסמטיים משמעותיים למעברים עם אנימציה בין חלונות במסך מלא וחלונות ב'תמונה בתוך תמונה'. מומלץ מאוד ליישם את כל שינויים רלוונטיים; אחרי שתעשו זאת, השינויים האלה ישתנו באופן אוטומטי בהתאם במסכים גדולים כמו מכשירים מתקפלים וטאבלטים, ללא צורך בפעולות נוספות.

אם האפליקציה לא כוללת עדכונים רלוונטיים, עדיין תתבצע העברה של 'תמונה בתוך תמונה' פונקציונליות, אבל האנימציות פחות מלוטשות. לדוגמה, העברה ממסך מלא למצב 'תמונה בתוך תמונה' עלול לגרום לחלון של 'תמונה בתוך תמונה' להיעלם לפני שהוא יופיע שוב לאחר שהמעבר יושלם.

השינויים האלה כרוכים בפעולות הבאות.

  • מעברים למצב 'תמונה בתוך תמונה' בצורה חלקה יותר בעזרת ניווט באמצעות תנועות
  • הגדרת sourceRectHint מתאים לכניסה למצב 'תמונה בתוך תמונה' וליציאה ממנו
  • השבתת שינוי חלק של הגודל בתוכן שאינו וידאו

ראה Android דוגמה של Kotlin PictureInPicture כדי לספק חוויית מעבר משופרת.

מעברים למצב 'תמונה בתוך תמונה' בצורה חלקה יותר בניווט באמצעות תנועות

החל מ-Android 12, הדגל setAutoEnterEnabled מספק אנימציה חלקה יותר למעבר לתוכן וידאו במצב 'תמונה בתוך תמונה' באמצעות תנועה ניווט – לדוגמה, כשמחליקים למעלה ממסך מלא.

צריך לפעול לפי השלבים הבאים כדי לבצע את השינוי הזה, ולעיין בדוגמה הזו הפניה:

  1. משתמשים ב-setAutoEnterEnabled כדי ליצור PictureInPictureParams.Builder:

    Kotlin

    setPictureInPictureParams(PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build())
    

    Java

    setPictureInPictureParams(new PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build());
    
  2. יש להתקשר אל setPictureInPictureParams עם הפרטים המעודכנים PictureInPictureParams מוקדם. האפליקציה לא ממתינה קריאה חוזרת של onUserLeaveHint (כמו שהייתם עושים ב-Android 11).

    לדוגמה, תוכל להתקשר אל setPictureInPictureParams את ההפעלה הראשונה וכל סרטון אחר, אם יחס הגובה-רוחב ישתנה.

  3. צריך להתקשר אל setAutoEnterEnabled(false), אבל רק לפי הצורך. לדוגמה, כנראה שלא תרצו להיכנס ל'תמונה בתוך תמונה' אם ההפעלה הנוכחית נמצאת במצב השהיה .

צריך להגדיר sourceRectHint מתאים לכניסה למצב 'תמונה בתוך תמונה' וליציאה ממנו

אנחנו מתחילים עם השקת התכונה 'תמונה בתוך תמונה' ב-Android 8.0, setSourceRectHint מציין את האזור של הפעילות שניתן לראות לאחר המעבר אל תמונה בתוך תמונה — לדוגמה, תחום הצפייה בסרטון בנגן וידאו.

עם Android 12, המערכת משתמשת ב-sourceRectHint כדי להטמיע אנימציה גם בכניסה למצב 'תמונה בתוך תמונה' וגם ביציאה ממנו.

כדי להגדיר בצורה נכונה את sourceRectHint לכניסה למצב 'תמונה בתוך תמונה' וליציאה ממנו:

  1. יצירה של PictureInPictureParams באמצעות הגבולות המתאימים כמו sourceRectHint. מומלץ לצרף גם שינוי הפריסה של ה-listener לנגן הווידאו:

    Kotlin

    val mOnLayoutChangeListener =
    OnLayoutChangeListener { v: View?, oldLeft: Int,
            oldTop: Int, oldRight: Int, oldBottom: Int, newLeft: Int, newTop:
            Int, newRight: Int, newBottom: Int ->
        val sourceRectHint = Rect()
        mYourVideoView.getGlobalVisibleRect(sourceRectHint)
        val builder = PictureInPictureParams.Builder()
            .setSourceRectHint(sourceRectHint)
        setPictureInPictureParams(builder.build())
    }
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener)
    

    Java

    private final View.OnLayoutChangeListener mOnLayoutChangeListener =
            (v, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight,
            newBottom) -> {
        final Rect sourceRectHint = new Rect();
        mYourVideoView.getGlobalVisibleRect(sourceRectHint);
        final PictureInPictureParams.Builder builder =
            new PictureInPictureParams.Builder()
                .setSourceRectHint(sourceRectHint);
        setPictureInPictureParams(builder.build());
    };
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener);
    
  2. אם יש צורך, מעדכנים את sourceRectHint לפני שהמערכת מפעילה את מעבר ליציאה. כשהמערכת עומדת לצאת ממצב 'תמונה בתוך תמונה', היררכיית התצוגות מסודרת לפי תצורת היעד שלה (למשל, מסך מלא). האפליקציה יכולה לצרף האזנה לשינוי פריסה לתצוגת הבסיס שלה. או תצוגת יעד (למשל התצוגה בנגן הווידאו) כדי לזהות את האירוע, מעדכנים את sourceRectHint לפני שהאנימציה מתחילה.

    Kotlin

    // Listener is called immediately after the user exits PiP but before animating.
    playerView.addOnLayoutChangeListener { _, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom ->
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            val sourceRectHint = Rect()
            playerView.getGlobalVisibleRect(sourceRectHint)
            setPictureInPictureParams(
                PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build()
            )
        }
    }
    

    Java

    // Listener is called right after the user exits PiP but before
    // animating.
    playerView.addOnLayoutChangeListener((v, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom) -> {
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            final Rect sourceRectHint = new Rect();
            playerView.getGlobalVisibleRect(sourceRectHint);
            setPictureInPictureParams(
                new PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build());
        }
    });
    

השבתת שינוי חלק של הגודל בתוכן שאינו וידאו

ב-Android 12 נוסיף את הדגל setSeamlessResizeEnabled, שמספק אנימציה חלקה יותר של עמעום הדרגתי כאשר משנים את הגודל של תוכן שאינו וידאו ב'תמונה בתוך תמונה' חלון. בעבר, שינוי הגודל של תוכן שאינו וידאו בחלון 'תמונה בתוך תמונה' היה יוצר פריטי מידע ויזואליים צורמים.

כדי להשבית בצורה חלקה את שינוי הגודל של תוכן שהוא לא וידאו:

Kotlin

setPictureInPictureParams(PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build())

Java

setPictureInPictureParams(new PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build());

טיפול בממשק המשתמש במהלך 'תמונה בתוך תמונה'

כשהפעילות נכנסת למצב 'תמונה בתוך תמונה' או יוצאת ממנו, המערכת קוראת Activity.onPictureInPictureModeChanged() או Fragment.onPictureInPictureModeChanged()

עליך לשנות את הקריאות החוזרות האלה כדי לצייר מחדש את רכיבי ממשק המשתמש של הפעילות. שמירה חשוב לזכור שבמצב 'תמונה בתוך תמונה' הפעילות שלך מוצגת בחלון קטן. המשתמשים לא יכולים לקיים אינטראקציה עם רכיבי ממשק המשתמש של האפליקציה כשהיא במצב 'תמונה בתוך תמונה' ועם הפרטים ייתכן שיהיה קשה לראות אלמנטים קטנים של ממשק המשתמש. פעילויות הפעלת וידאו עם ממשק משתמש מינימלי מספק את חוויית המשתמש הטובה ביותר.

אם האפליקציה שלך צריכה לספק פעולות מותאמות אישית ל'תמונה בתוך תמונה', כדאי לעיין בקטע הוספה הפקדים בדף הזה. הסירו רכיבים אחרים בממשק המשתמש לפני הפעילות עוברת למצב 'תמונה בתוך תמונה' ומשחזרת אותן כשהפעילות הופכת למסך מלא שוב:

Kotlin

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,
                                           newConfig: Configuration) {
    if (isInPictureInPictureMode) {
        // Hide the full-screen UI (controls, etc.) while in PiP mode.
    } else {
        // Restore the full-screen UI.
    }
}

Java

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

הוספת פקדים

החלון של 'תמונה בתוך תמונה' יכול להציג פקדים כשהמשתמש פותח את תפריט החלון (על ידי להקיש על החלון במכשיר נייד, או לבחור את התפריט מהטלוויזיה מרחוק).

אם לאפליקציה יש מדיה פעילה סשן, ואז להפעיל, יופיעו הפקדים 'השהיה', 'הבא' ו'הקודם'.

אפשר גם לציין פעולות מותאמות אישית באופן מפורש על ידי יצירת PictureInPictureParams עם PictureInPictureParams.Builder.setActions() לפני הכניסה למצב 'תמונה בתוך תמונה', ולהעביר את הפרמטרים כשאתם נכנסים למצב 'תמונה בתוך תמונה' באמצעות enterPictureInPictureMode(android.app.PictureInPictureParams) או setPictureInPictureParams(android.app.PictureInPictureParams). חשוב להיזהר. אם תנסה להוסיף יותר מ- getMaxNumPictureInPictureActions() מקבלים רק את המספר המקסימלי.

המשך הפעלת הסרטון במצב 'תמונה בתוך תמונה'

כשהפעילות עוברת למצב 'תמונה בתוך תמונה', המערכת מציבה את הפעילות וקורא לפעילות onPause(). קמפיינים של מודעות וידאו אין להשהות את ההפעלה, אלא להמשיך את ההפעלה אם הפעילות הושהתה במהלך המעבר למצב 'תמונה בתוך תמונה'.

ב-Android 7.0 ואילך, עליך להשהות ולהמשיך את הפעלת הסרטון כאשר קוראת לפעילות שלכם onStop() ו- onStart() כך, אפשר להימנע מהצורך לבדוק אם האפליקציה נמצאת במצב 'תמונה בתוך תמונה' בonPause(). להמשיך את ההפעלה באופן מפורש.

אם לא הגדרתם את הדגל setAutoEnterEnabled ל-true ואתם צריכים אפשר להשהות את ההפעלה בהטמעה של onPause(). כדי לבדוק את מצב 'תמונה בתוך תמונה', צריך להתקשר isInPictureInPictureMode() ולטפל בהפעלה בהתאם לדרישות. לדוגמה:

Kotlin

override fun onPause() {
    super.onPause()
    // If called while in PiP mode, do not pause playback
    if (isInPictureInPictureMode) {
        // Continue playback
    } else {
        // Use existing playback logic for paused Activity behavior.
    }
}

Java

@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.
        ...
    }
}

כשהפעילות עוברת ממצב 'תמונה בתוך תמונה' חזרה למצב מסך מלא, המערכת ממשיך את הפעילות ומתקשר onResume().

שימוש בפעילות הפעלה אחת בשביל 'תמונה בתוך תמונה'

באפליקציה שלך, המשתמש יכול לבחור סרטון חדש כשהוא מחפש תוכן במסך הראשי, בזמן שהפעילות של הפעלת הסרטונים נמצאת במצב 'תמונה בתוך תמונה'. הפעלת הסרטון החדש בפעילות ההפעלה הקיימת במצב מסך מלא, במקום להפעיל פעילות חדשה שעשויה לבלבל את המשתמש.

כדי לוודא שפעילות אחת משמשת לבקשות הפעלת סרטון ובוצעה שינוי להיכנס למצב 'תמונה בתוך תמונה' או לצאת ממנו לפי הצורך, להגדיר את android:launchMode של הפעילות לערך singleTask במניפסט:

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

בפעילות שלכם, משנים את ההגדרות onNewIntent() ולטפל בסרטון החדש, ולהפסיק את ההפעלה של הסרטון הקיים במקרה הצורך.

שיטות מומלצות

יכול להיות ש'תמונה בתוך תמונה' מושבתת במכשירים עם זיכרון RAM נמוך. לפני שהאפליקציה משתמשת ב'תמונה בתוך תמונה', מומלץ להתקשר ולבדוק אם יש תמיכה hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)

'תמונה בתוך תמונה' מיועדת לפעילויות שמופעלות בהן סרטונים במסך מלא. כשמחליפים את פעילות במצב 'תמונה בתוך תמונה' ולא להציג שום דבר מלבד תוכן וידאו. מעקב אחרי הפעילות עוברת למצב 'תמונה בתוך תמונה' ומסתירה רכיבים בממשק המשתמש, כפי שמתואר במאמר טיפול בממשק המשתמש במהלך 'תמונה בתוך תמונה'.

כשפעילות נמצאת במצב 'תמונה בתוך תמונה', כברירת מחדל לא ניתן להתמקד בקלט. שפת תרגום קבלת אירועי קלט במצב 'תמונה בתוך תמונה', משתמשים MediaSession.setCallback() מידע נוסף על השימוש ב-setCallback() זמין במאמר הצגת התכונה 'מה שומעים עכשיו?' .

כשהאפליקציה במצב 'תמונה בתוך תמונה', הפעלת וידאו בחלון 'תמונה בתוך תמונה' עלולה לגרום לאודיו להפריע לאפליקציה אחרת, כמו אפליקציה של נגן מוזיקה או אפליקציית חיפוש קולי. כדי למנוע מצב כזה, מבקשים מיקוד אודיו כשמפעילים את הסרטון, ולוחצים על התראות על שינוי של מיקוד האודיו, כפי שמתואר בניהול האודיו מיקוד. אם קיבלת התראה של אובדן מיקוד אודיו במצב 'תמונה בתוך תמונה', משהים או מפסיקים את הפעלת הסרטון.

כשהאפליקציה שלך עומדת להיכנס ל'תמונה בתוך תמונה', חשוב לשים לב רק הפעילות המובילה נכנסת תמונה בתוך תמונה. במצבים מסוימים, למשל במכשירים עם ריבוי חלונות, ייתכן שהפעילות שלמטה תוצג כעת ותהיה גלויה שוב לצד את הפעילות של 'תמונה בתוך תמונה'. עליך לטפל במקרה הזה בהתאם, כולל פעילות מתחת לקבלת onResume() או התקשרות חזרה של onPause(). כמו כן ייתכן שהמשתמש יקיים אינטראקציה עם הפעילות. לדוגמה, אם יש לך פעילות של רשימת סרטונים שמוצגת ושל הפעילות עם סרטונים שמופעלת במצב 'תמונה בתוך תמונה', משתמש יכול לבחור סרטון חדש מהרשימה, והפעילות של 'תמונה בתוך תמונה' אמורה להתעדכן בהתאם.

קוד לדוגמה נוסף

כדי להוריד אפליקציה לדוגמה כתובה ב-Kotlin, עבור אל Android PictureInPicture Sample (קוטלין).