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

تتيح الشاشات الكبيرة غير المطوية والحالات المطوية الفريدة للمستخدمين تقديم تجارب جديدة للمستخدمين على الأجهزة القابلة للطي. لإظهار ظهور تطبيقك في طيّه، يمكنك استخدام مكتبة 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 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 windowInfoRepo 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()
                        .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، يمكن لتطبيقك استخدام أوضاع الوضعية، مثل وضع "التثبيت على سطح مستوٍ" حيث يكون الهاتف على سطح مستوٍ، ويتم وضع المفصّلة في وضع أفقي، وأن تكون الشاشة القابلة للطي مفتوحة جزئيًا.

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

تطبيق مشغّل فيديو في وضع &quot;التثبيت على سطح مستوٍ&quot;

استخدِم 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);
}

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

أمثلة

  • تطبيق MediaPlayerActivity: تعرَّف على طريقة استخدام Media3 Exoplayer وWindowManager لإنشاء مشغّل فيديو قابل للطي.

  • استعرِض تجربة استخدام الكاميرا: تعرَّف على طريقة تطبيق وضع "التثبيت على سطح مستوٍ" لتطبيقات التصوير. اعرض عدسة الكاميرا في النصف العلوي من الشاشة، في الجزء المرئي من الصفحة، وعناصر التحكم في النصف السفلي من الجزء السفلي غير المرئي من الصفحة.

وضع الكتاب

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

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

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

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 استرداد مقاييس النوافذ الحالية والأقصى. تمامًا مثل النظام الأساسي WindowMetrics الذي تم تقديمه في المستوى 30 من واجهة برمجة التطبيقات، يوفّر WindowsManager WindowMetrics حدودًا للنوافذ، إلّا أنّ واجهة برمجة التطبيقات متوافقة مع الأنظمة القديمة وصولاً إلى المستوى 14 من واجهة برمجة التطبيقات.

تعرَّف على كيفية إتاحة أحجام النوافذ المختلفة في إتاحة أحجام الشاشات المختلفة.

مراجع إضافية

عيّنات

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

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