جعل تطبيقك مطوّلاً

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

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

معلومات النافذة

تعرض واجهة WindowInfoTracker في Jetpack WindowManager معلومات تخطيط النافذة. تعرض الطريقة windowLayoutInfo() في الواجهة دفقًا من بيانات WindowLayoutInfo التي تُعلم تطبيقك بحالة الطي لجهاز قابل للطي. ينشئ الإجراء WindowInfoTracker#getOrCreate() مثيلاً من WindowInfoTracker.

توفّر WindowManager إمكانية جمع بيانات WindowLayoutInfo باستخدام تدفّقات Kotlin وعمليات ردّ الاتصال في Java.

مسارات Kotlin

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

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

عمليات إعادة الاستدعاء في Java

تتيح لك طبقة التوافق مع الدوالّ رد الاتصال المضمّنة في تبعية androidx.window:window-java جمع تحديثات WindowLayoutInfo بدون استخدام تدفق Kotlin. يتضمّن العنصر فئة WindowInfoTrackerCallbackAdapter، التي تعدّل WindowInfoTracker لتتيح تسجيل (وإلغاء تسجيل) عمليات رد الاتصال لتلقّي تحديثات WindowLayoutInfo، على سبيل المثال:

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoTracker
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           SplitLayoutActivity.this.runOnUiThread( () -> {
               // Use newLayoutInfo to update the layout.
           });
       }
   }
}

توافُق RxJava

إذا كنت تستخدم RxJava (الإصدار 2 أو 3)، يمكنك الاستفادة من العناصر التي تتيح لك استخدام Observable أو Flowable لجمع تحديثات WindowLayoutInfo بدون استخدام تدفق Kotlin.

تتضمّن طبقة التوافق التي توفّرها التبعيتان androidx.window:window-rxjava2 وandroidx.window:window-rxjava3 الطريقتَين WindowInfoTracker#windowLayoutInfoFlowable() وWindowInfoTracker#windowLayoutInfoObservable()، ما يتيح لتطبيقك تلقّي تحديثات WindowLayoutInfo، على سبيل المثال:

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable.
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates.
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the layout.
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose of the WindowLayoutInfo observable.
        disposable?.dispose()
   }
}

ميزات الشاشات القابلة للطي

تتيح الفئة WindowLayoutInfo في Jetpack WindowManager ميزات نافذة العرض كقائمة بعناصر DisplayFeature.

FoldingFeature هو نوع من DisplayFeature يقدّم معلومات حول الشاشات القابلة للطي، بما في ذلك السمات التالية:

  • state: حالة الجهاز عند طيّه، FLAT أو HALF_OPENED

  • orientation: اتجاه الطي أو المفصل، HORIZONTAL أو VERTICAL

  • occlusionType: ما إذا كان الطي أو المفصلة يخفيان جزءًا من الشاشة، NONE أو FULL

  • isSeparating: ما إذا كان الطي أو المفصل ينشئان مساحتَي عرض منطقيتَين، صحيح أو خطأ

الجهاز القابل للطي الذي HALF_OPENED يعرض دائمًا القيمة isSeparating على أنّها صحيحة لأنّ الشاشة مقسّمة إلى منطقتَي عرض. بالإضافة إلى ذلك، تكون قيمة isSeparating صحيحة دائمًا على جهاز بشاشتَين عندما يمتد التطبيق على كلتا الشاشتَين.

تمثّل السمة FoldingFeature bounds (الموروثة من DisplayFeature) المستطيل المحيط بميزة قابلة للطي، مثل الطي أو المفصلة. يمكن استخدام الحدود لتحديد موضع العناصر على الشاشة بالنسبة إلى الميزة:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from WindowInfoTracker when the lifecycle is
            // STARTED and stops collection when the lifecycle is STOPPED.
            WindowInfoTracker.getOrCreate(this@MainActivity)
                .windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo ->
                    // New posture information.
                    val foldingFeature = layoutInfo.displayFeatures
                        .filterIsInstance<FoldingFeature>()
                        .firstOrNull()
                    // Use information from the foldingFeature object.
                }
        }
    }
}

Java

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
                new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    // ...
    windowInfoTracker =
            new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
    super.onStart();
    windowInfoTracker.addWindowLayoutInfoListener(
            this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
    super.onStop();
    windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout.
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                // Use information from the feature object.
            }
        }
    }
}

وضع الجهاز على سطح مستوٍ

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

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

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

استخدِم FoldingFeature.State وFoldingFeature.Orientation لتحديد ما إذا كان الجهاز في وضع مسطّح على الطاولة:

Kotlin

fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

Java

boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

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

في نظام التشغيل Android 15 (المستوى 35 من واجهة برمجة التطبيقات) والإصدارات الأحدث، يمكنك استدعاء واجهة برمجة تطبيقات متزامنة لرصد ما إذا كان الجهاز يتيح وضعية سطح الطاولة بغض النظر عن الحالة الحالية للجهاز.

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

Kotlin

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // Device supports tabletop posture.
    }
}

أمثلة

وضع الكتاب

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

ويمكن استخدامها أيضًا للتصوير إذا أردت التقاط صور بنسبة عرض إلى ارتفاع مختلفة بدون استخدام اليدين.

يمكنك تنفيذ وضعية الكتاب باستخدام التقنيات نفسها المستخدَمة لوضعية سطح الطاولة. والفرق الوحيد هو أنّ الرمز يجب أن يتحقّق من أنّ اتجاه ميزة الطي عمودي بدلاً من أفقي:

Kotlin

fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

Java

boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

تغييرات حجم النافذة

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

تتيح لك فئة WindowMetricsCalculator في Jetpack WindowManager استرداد مقاييس النافذة الحالية والحد الأقصى لها. على غرار WindowMetrics الذي تم تقديمه في المستوى 30 من واجهة برمجة التطبيقات، يوفّر WindowManager WindowMetrics حدود النافذة، ولكن تتوافق واجهة برمجة التطبيقات مع الإصدارات القديمة وصولاً إلى المستوى 14 من واجهة برمجة التطبيقات.

راجِع مقالة استخدام فئات أحجام النوافذ.

مراجع إضافية

نماذج

  • ‫Jetpack WindowManager: مثال على كيفية استخدام مكتبة Jetpack WindowManager
  • Jetcaster : تنفيذ وضعية الجهاز اللوحي باستخدام Compose

الدروس التطبيقية حول الترميز