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

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

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

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

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

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

مسارات Kotlin

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

class DisplayFeaturesActivity : ComponentActivity() {

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

        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 = ActivityRxBinding.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 من واجهة برمجة التطبيقات) والإصدارات الأحدث، يمكنك استدعاء واجهة برمجة تطبيقات متزامنة لرصد ما إذا كان الجهاز يتيح وضع "على المنضدة" بغض النظر عن الحالة الحالية للجهاز.

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

Kotlin

if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // 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

اختبارات الرموز