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

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

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

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

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

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

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

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

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

מצב מסך כפול

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

תרחיש לדוגמה הוא תרגום שיחה פעילה באמצעות התכונה Dual Screen.

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

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

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

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

מגניב

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;

כך מאתחלים את המשתנים ב-method 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

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

הקריאה החוזרת 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);
}

כדי לשמור על עקביות בסביבה העסקית, תוכלו להשתמש בסמל הרשמי של Dual Screen כדי לציין למשתמשים איך להפעיל או להשבית את מצב 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}");
    }
}

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

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