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

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

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

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

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

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

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

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

אפשר לנסות את מצב המסך האחורי באפליקציית המצלמה של Pixel Fold. אפשר לראות דוגמה להטמעה ב-codelab Optimize your camera app on foldable devices with Jetpack WindowManager (אופטימיזציה של אפליקציית המצלמה במכשירים מתקפלים באמצעות Jetpack WindowManager).

מצב מסך מפוצל

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

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

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

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

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

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

Groovy

dependencies {
    // TODO: Define window_version in your project's build configuration.
    implementation "androidx.window:window:$window_version"
}

Kotlin

dependencies {
    // Define window_version in your project's build configuration.
    implementation("androidx.window:window:$window_version")
}

נקודת הכניסה היא WindowAreaController, שמספקת את המידע וההתנהגות שקשורים להזזת חלונות בין מסכים או בין אזורי תצוגה במכשיר. ‫WindowAreaController lets you query the list of available WindowAreaInfo objects.

אפשר להשתמש ב-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 משתמש בגישת listener: כששולחים בקשה להצגת התוכן במסך השני של מכשיר מתקפל, מתחילים סשן שמוחזר באמצעות השיטה onSessionStarted() של ה-listener. כשסוגרים את הסשן, מקבלים אישור בשיטה onSessionEnded().

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

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

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

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

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

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);
}

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

דוגמה שעובדת אפשר לראות ב-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 toggleRearDisplayMode() {
    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, וקל יותר להטמיע אותה כי הקריאה החוזרת לא מקבלת רכיב presenter שמאפשר להציג תוכן באזור חלון, אלא מעבירה את כל הפעילות לאזור אחר:

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}");
    }
}

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

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