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

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

يتم دائمًا الإبلاغ عن isSeparating على الجهاز القابل للطي باعتباره "صحيح" بسبب HALF_OPENED لأن الشاشة مقسمة إلى منطقتَي عرض. بالإضافة إلى ذلك، يكون الخيار 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);
}

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

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

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

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

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

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

تتيح لك فئة Jetpack WindowManager WindowMetricsCalculator retrieving current and maximum window metrics. مثل النظام الأساسي WindowMetrics الذي تم تقديمه في المستوى 30 لواجهة برمجة التطبيقات، يقدّم WindowManager WindowMetrics حدود النافذة، ولكن واجهة برمجة التطبيقات متوافقة مع الإصدارات القديمة حتى المستوى 14 لواجهة برمجة التطبيقات.

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

مصادر إضافية

نماذج

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

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