תמיכה במצבי מסך מתקפל

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

מצב המסך האחורי

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

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

כדי להפעיל את מצב המסך האחורי, המשתמשים מגיבים לתיבת דו-שיח כדי לאפשר לאפליקציה: מעבר בין מסכים, לדוגמה:

איור 1. תיבת דו-שיח של המערכת שמאפשרת להפעיל את מצב התצוגה האחורית.

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

אפשר לנסות את מצב המסך האחורי באפליקציית המצלמה של Pixel Fold. לצפייה בדוגמה ב-Codelab אופטימיזציה של אפליקציית המצלמה במכשירים מתקפלים עם Jetpack windowManager.

מצב שני מסכים

במצב מסך מפוצל אפשר להציג תוכן בשני המסכים של מכשיר מתקפל בו-זמנית. מצב שני מסכים זמין במכשירי Pixel Fold עם Android 14 (רמת API 34) ומעלה.

תרחיש לדוגמה הוא 'תרגום שיחה פעילה' ב-Dual Screen.

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

הפעלת המצבים באופן פרוגרמטי

אפשר לגשת למצב המסך האחורי ולמצב המסך הכפול דרך ממשקי ה-API של Jetpack WindowManager, החל מגרסה 1.2.0-beta03 של הספרייה.

מוסיפים את התלות של windowManager לקובץ המודול build.gradle של האפליקציה:

Groovy

dependencies {
    implementation "androidx.window:window:1.2.0-beta03"
}

Kotlin

dependencies {
    implementation("androidx.window:window:1.2.0-beta03")
}

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

משתמשים ב-WindowAreaInfo כדי לגשת ל-WindowAreaSession, ממשק שמייצג תכונה של אזור חלון פעיל. יש להשתמש ב-WindowAreaSession כדי לקבוע הזמינות של WindowAreaCapability ספציפי.

כל יכולת קשורה ל-WindowAreaCapability.Operation מסוים. בגרסה 1.2.0-beta03, Jetpack WindowManager תומך בשני סוגים של פעולות:

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

Kotlin

private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var windowAreaSession: WindowAreaSession? = null
private var windowAreaInfo: WindowAreaInfo? = null
private var capabilityStatus: WindowAreaCapability.Status =
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED

private val dualScreenOperation = WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA

Java

private WindowAreaControllerCallbackAdapter windowAreaController = null;
private Executor displayExecutor = null;
private WindowAreaSessionPresenter windowAreaSession = null;
private WindowAreaInfo windowAreaInfo = null;
private WindowAreaCapability.Status capabilityStatus  =
        WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED;

private WindowAreaCapability.Operation dualScreenOperation =
        WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA;
private WindowAreaCapability.Operation rearDisplayOperation =
        WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA;

כך מאתחלים את המשתנים בשיטה onCreate() של הפעילות:

Kotlin

displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        windowAreaController.windowAreaInfos
            .map { info -> info.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING } }
            .onEach { info -> windowAreaInfo = info }
            .map { it?.getCapability(operation)?.status ?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
            .distinctUntilChanged()
            .collect {
                capabilityStatus = it
            }
    }
}

Java

displayExecutor = ContextCompat.getMainExecutor(this);
windowAreaController = new WindowAreaControllerCallbackAdapter(WindowAreaController.getOrCreate());
windowAreaController.addWindowAreaInfoListListener(displayExecutor, this);

windowAreaController.addWindowAreaInfoListListener(displayExecutor,
  windowAreaInfos -> {
    for(WindowAreaInfo newInfo : windowAreaInfos){
        if(newInfo.getType().equals(WindowAreaInfo.Type.TYPE_REAR_FACING)){
            windowAreaInfo = newInfo;
            capabilityStatus = newInfo.getCapability(presentOperation).getStatus();
            break;
        }
    }
});

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

Kotlin

when (capabilityStatus) {
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
      // The selected display mode is not supported on this device.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
      // The selected display mode is not available.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
      // The selected display mode is available and can be enabled.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
      // The selected display mode is already active.
    }
    else -> {
      // The selected display mode status is unknown.
    }
}

Java

if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED)) {
  // The selected display mode is not supported on this device.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE)) {
  // The selected display mode is not available.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
  // The selected display mode is available and can be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE)) {
  // The selected display mode is already active.
}
else {
  // The selected display mode status is unknown.
}

מצב מסך כפול

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

Kotlin

fun toggleDualScreenMode() {
    if (windowAreaSession != null) {
        windowAreaSession?.close()
    }
    else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.presentContentOnWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaPresentationSessionCallback = this
            )
        }
    }
}

Java

private void toggleDualScreenMode() {
    if(windowAreaSession != null) {
        windowAreaSession.close();
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.presentContentOnWindowArea( token, this, displayExecutor, this);
    }
}

שימו לב לשימוש בפעילות העיקרית של האפליקציה בתור ארגומנט WindowAreaPresentationSessionCallback.

ה-API משתמש בגישה של מאזין: כששולחים בקשה להצגת התוכן במסך השני של מכשיר מתקפל, מתחילים סשן שמוחזר באמצעות השיטה onSessionStarted() של המאזין. כשסוגרים את הסשן, מקבלים אישור בשיטה onSessionEnded().

כדי ליצור את ה-listener, צריך להטמיע את WindowAreaPresentationSessionCallback ממשק:

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

המאזין צריך להטמיע את השיטות onSessionStarted(),‏ onSessionEnded(), ו-onContainerVisibilityChanged(). התראות לגבי שיטות הקריאה החוזרת את סטטוס הסשן ולאפשר לך לעדכן את האפליקציה בהתאם.

הקריאה החוזרת של onSessionStarted() מקבלת ערך של WindowAreaSessionPresenter בתור ארגומנט. הארגומנט הוא הקונטיינר שמאפשר לגשת לאזור חלון ולהציג את התוכן. המערכת יכולה לסגור את שיתוף המסך באופן אוטומטי כשהמשתמש יוצא מחלון האפליקציה הראשי, או שאפשר לסגור את שיתוף המסך על ידי קריאה לפונקציה WindowAreaSessionPresenter#close().

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

Kotlin

override fun onSessionStarted(session: WindowAreaSessionPresenter) {
    windowAreaSession = session
    val view = TextView(session.context)
    view.text = "Hello world!"
    session.setContentView(view)
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

override fun onContainerVisibilityChanged(isVisible: Boolean) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = $isVisible")
}

Java

@Override
public void onSessionStarted(@NonNull WindowAreaSessionPresenter session) {
    windowAreaSession = session;
    TextView view = new TextView(session.getContext());
    view.setText("Hello world, from the other screen!");
    session.setContentView(view);
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

@Override public void onContainerVisibilityChanged(boolean isVisible) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = " + isVisible);
}

כדי לשמור על עקביות בסביבה העסקית, אפשר להשתמש במסמך הרשמי של Dual Screen סמל שמציין למשתמשים איך להפעיל או להשבית את מצב מסך כפול.

דוגמה עובדת מופיעה בקובץ DualScreenActivity.kt.

מצב תצוגה אחורית

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

Kotlin

fun toggleRearDisplayMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo?.getActiveSession(
                operation
            )
        }
        windowAreaSession?.close()
    } else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.transferActivityToWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaSessionCallback = this
            )
        }
    }
}

Java

void toggleDualScreenMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo.getActiveSession(
                operation
            )
        }
        windowAreaSession.close()
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.transferActivityToWindowArea(token, this, displayExecutor, this);
    }
}

במקרה כזה, הפעילות שמוצגת משמשת כ-WindowAreaSessionCallback, שקל יותר להטמיע כי פונקציית ה-callback לא מקבלת מציג שמאפשר להציג תוכן באזור חלון, אלא מעבירה את כל הפעילות לאזור אחר:

Kotlin

override fun onSessionStarted() {
    Log.d(logTag, "onSessionStarted")
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

Java

@Override public void onSessionStarted(){
    Log.d(logTag, "onSessionStarted");
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

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

מקורות מידע נוספים