שימוש בספריית Jetpack Picture-in-Picture

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

ספריית Jetpack של תמונה בתוך תמונה (PiP) מאפשרת להשתמש בממשקי ה-API הקיימים של PiP, ומטפלת בכמה אתגרים ואי-התאמות מרכזיים בסביבת Android:

  • פיצול מערכת ההפעלה: הספרייה מטפלת אוטומטית בהבדלים בקריאות ל-PiP API בגרסאות שונות של Android, למשל שימוש ב-enterPictureInPictureMode לפני Android 12 וב-isAutoEnterEnabled אחרי, כך שהמפתחים לא צריכים לנהל הבדלים בגרסאות.
  • פרמטרים שגויים של PiP: הכלי מספק פתרון אחיד להגדרה נכונה של פרמטרים של PiP, למשל setSourceRectHint, כדי ליצור אנימציות חלקות ואיכותיות במהלך הפעלת מדיה.
  • קריאות חוזרות (callback) מאוחדות למצב 'תמונה בתוך תמונה': הקריאות החוזרות onPictureInPictureModeChanged ו-onPictureInPictureUiStateChanged אוחדו לממשק קריאות חוזרות (callback) מאוחד יחיד (PictureInPictureDelegate.OnPictureInPictureEventListener) כדי לפשט את ניהול המצב וממשק המשתמש.
  • צמצום של קוד שחוזר על עצמו (boilerplate): הספרייה מצמצמת את כמות הקוד שחוזר על עצמו, ומציעה קבוצות מוגדרות מראש של RemoteActions לתרחישי שימוש נפוצים, כמו רכיבי UI להפעלה ופעולות בשיחות וידאו.
  • הכנה לעתיד: תכונות נוספות של PiP מועברות דרך ספריית Jetpack, כך שמשתמשים יכולים לגשת לפונקציונליות נוספת במאמץ מינימלי או ללא מאמץ בכלל.

תהליך העבודה של המיגרציה

זיהוי קטגוריית התרחיש לדוגמה של האפליקציה והלוגיקה של התמונה בתוך תמונה (PiP) מדור קודם:

קטגוריות: הפעלת סרטון, ניווט או שיחת וידאו.

Legacy PiP Logic to Identify:

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams.

2. הגדרה של AndroidManifest

כדי למנוע הפעלות מחדש מיותרות, חשוב לוודא שהפעילות שנכנסת למצב תמונה בתוך תמונה מצהירה על תמיכה ב-AndroidManifest.xml עם configChanges הנדרשים:

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

3. הגדרת הסביבה

מוסיפים את יחסי התלות הנדרשים לקובץ build.gradle:

dependencies {
implementation("androidx.core:core:1.18.0")
implementation("androidx.activity:activity:1.13.0")
implementation("androidx.core:core-pip:1.0.0-alpha02") }

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

4. בחירת תבנית ואתחול

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

  • ניווט ושיחות וידאו: BasicPictureInPicture; בדרך כלל אין תמיכה בשינוי גודל חלק, ולא צריך רמז לגבי מלבן המקור.
  • הפעלת סרטון: VideoPlaybackPictureInPicture; מעקב אוטומטי אחרי גבולות התצוגה של הנגן כדי לקבל רמז לגבי המלבן של המקור, ושינוי גודל חלק כברירת מחדל.

כדי להשתמש בספריית Jetpack, צריך להחליף את ההטמעה הקיימת של תמונה בתוך תמונה (PiP) בהטמעות של ממשקי Jetpack API. מורכבות ההטמעה והעלות שלה משתנות בהתאם להטמעה הנוכחית של האפליקציה.

בקטעים הבאים מתוארים כמה תרחישי שימוש אופייניים של PiP ושלבי ההטמעה הנדרשים:

האפליקציה מעדכנת את הספרייה לגבי המצב הפעיל או הלא פעיל של הניווט ומגדירה את יחס הגובה-רוחב. ספריית Jetpack מטפלת בכל השאר.

ההבדלים העיקריים:

  1. אין צורך להבחין בין הזנה אוטומטית לבין הזנה מדור קודם בצד האפליקציה.
  2. ממשקי קריאה חוזרת מאוחדים.
  3. כלי חדש ליצירת PictureInPictureParams לתאימות לאחור.

שיחת וידאו

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

ההבדלים העיקריים:

  1. אין צורך להבחין בין הזנה אוטומטית לבין הזנה מדור קודם בצד האפליקציה.
  2. ממשקי קריאה חוזרת מאוחדים.
  3. כלי חדש ליצירת PictureInPictureParams לתאימות לאחור.
  4. סמלי פעולות סטנדרטיים לשיחות וידאו.

5. העברת קוד

  • לוגיקת כניסה: מחליפים לוגיקה ספציפית ל-API, כמו setAutoEnterEnabled ל-Android 12 ומעלה או onUserLeaveHint ל-Android 11 ומטה, ב-setEnabled. הפעלת הטריגר הזה בכל פעם שסטטוס הזכאות לתמונה בתוך תמונה משתנה.
  • Callbacks: איחוד של onPictureInPictureModeChanged (החלפת פריסה) ושל onPictureInPictureUiStateChanged (אנימציה/מצבים) ל-callback מאוחד שמבוסס על אירועים onPictureInPictureEvent.
  • פעולות ופרמטרים: מעדכנים את הפרמטרים באמצעות setActions ו-setAspectRatio במופע של התבנית בכל פעם שהם משתנים.

דפוסי הטמעה לדוגמה

דוגמאות להטמעות.

ניווט ושיחות וידאו

class NavOrVideoCallJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: BasicPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = BasicPictureInPicture(this)
        // BasicPictureInPicture is ideal for Navigation and Video call use cases.
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTERED -> { /* Toggle to PiP layout */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Toggle to Full-screen layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* Optional: PiP is stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* Optional: PiP is unstashed */ }
        }
    }
}

הפעלת סרטון

class VideoPlaybackJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = VideoPlaybackPictureInPicture(this)
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
            ContentScreen(pictureInPictureImpl)
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> { /* Hide overlays */ }
            PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> { /* Animation finished */ }
            PictureInPictureDelegate.Event.ENTERED -> { /* Switch to PiP layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* PiP stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* PiP unstashed */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Return to full-screen */ }
        }
    }

    @Composable
    fun ContentScreen(pipController: VideoPlaybackPictureInPicture) {
        DisposableEffect(pipController) {
            onDispose {
                pipController.close()
            }
        }
    }
}