إتاحة أوضاع العرض القابلة للطي

تقدّم الأجهزة القابلة للطي تجارب مشاهدة فريدة. يتيح لك وضع "العرض الخلفي" و"وضع الشاشة المزدوجة" إنشاء ميزات عرض خاصة للأجهزة القابلة للطي، مثلاً معاينة الصور الذاتية بالكاميرا الخلفية والشاشات المتزامنة والمختلفة على الشاشات الداخلية والخارجية.

وضع العرض الخلفي

عندما يكون الجهاز القابل للطي غير مطوي، يتم عادةً تشغيل الشاشة الداخلية فقط. يتيح لك وضع العرض الخلفي نقل أحد الأنشطة إلى الشاشة الخارجية لجهاز قابل للطي، بحيث يكون الجهاز عادةً بعيدًا عن المستخدم عندما يكون الجهاز غير مطوي. يتم إيقاف الشاشة الداخلية تلقائيًا.

هناك تطبيق جديد يتمثل في عرض معاينة الكاميرا على الشاشة الخارجية، حتى يتمكن المستخدمون من التقاط صور ذاتية باستخدام الكاميرا الخلفية، والتي عادةً ما توفر أداءً أفضل بكثير في التقاط الصور.

لتفعيل وضع العرض الخلفي، يستجيب المستخدمون لمربّع الحوار للسماح للتطبيق بتبديل الشاشات، على سبيل المثال:

الشكل 1. مربع حوار النظام للسماح ببدء وضع العرض الخلفي.

ينشئ النظام مربع الحوار، لذا لا حاجة إلى إجراء أي تطوير من جانبك. تظهر مربعات حوار مختلفة حسب حالة الجهاز، على سبيل المثال، يوجّه النظام المستخدمين إلى فتح الجهاز إذا كان الجهاز مغلقًا. لا يمكنك تخصيص مربّع الحوار، ولكن يمكن أن يختلف على الأجهزة التابعة لمصنّعين أصليين مختلفين.

يمكنك تجربة وضع العرض الخلفي باستخدام تطبيق كاميرا Pixel Fold. يمكنك الاطّلاع على نموذج لتنفيذه في الدرس التطبيقي حول الترميز فتح تجربة الكاميرا.

وضع Dual Screen

يتيح لك وضع Dual Screen عرض المحتوى على كلتا شاشتَي العرض القابل للطي في الوقت نفسه. يتوفّر وضع Dual Screen على هواتف Pixel Fold التي تعمل بنظام التشغيل Android 14 (المستوى 34 لواجهة برمجة التطبيقات) أو الإصدارات الأحدث.

من أمثلة حالات الاستخدام ميزة "الترجمة الفورية" كشاشة مزدوجة.

الشكل 2. مترجم فوري ثنائي الشاشة يعرض محتوًى مختلفًا على الشاشة الأمامية والخلفية

تفعيل الأوضاع آليًا

يمكنك استخدام وضع العرض الخلفي ووضع الشاشة المزدوجة من خلال واجهات برمجة تطبيقات 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-الإصدار التجريبي 03، يتيح 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 currently available to be enabled.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
      // The selected display mode is currently available to 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 currently available to be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
  // The selected display mode is currently available to 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.    
}

وضع Dual Screen

يُغلق المثال التالي الجلسة إذا كانت الميزة نشطة من قبل، أو تستدعي دالة 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.

تعتمد واجهة برمجة التطبيقات أسلوب استماع: عندما تقدّم طلبًا لعرض المحتوى على شاشة العرض الأخرى القابلة للطي، تبدأ جلسة يتم عرضها من خلال طريقة onSessionStarted() التي يستخدمها المستمع. عند إغلاق الجلسة، ستتلقّى رسالة تأكيد بطريقة onSessionEnded().

لإنشاء أداة المستمع، نفِّذ واجهة WindowAreaPresentationSessionCallback:

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

على المستمع تنفيذ طرق 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 لإعلام المستخدمين بكيفية تفعيل وضع الشاشة المزدوجة أو إيقافه.

للحصول على عينة عملية، اطلع على 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, التي يسهل تنفيذها لأنّ معاودة الاتصال لا تتلقّى مقدّمًا يسمح بعرض المحتوى في منطقة نافذة، بل ينقل النشاط بالكامل إلى منطقة أخرى بدلاً من ذلك:

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

للحفاظ على الاتّساق في المنظومة المتكاملة، استخدِم الرمز الرسمي للكاميرا الخلفية لإطلاع المستخدمين على كيفية تفعيل وضع العرض الخلفي أو إيقافه.

مراجع إضافية