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

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

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

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

يوفِّر تطبيق WindowManager إمكانية جمع بيانات WindowLayoutInfo باستخدام عمليات استدعاء لغة Kotlin Flows و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 من واجهة برمجة التطبيقات، يوفّر WindowManager WindowMetrics حدودًا للنوافذ، غير أنّ واجهة برمجة التطبيقات تتوافق مع الأنظمة القديمة وصولاً إلى المستوى 14 من واجهة برمجة التطبيقات.

راجِع فئات حجم النوافذ.

مصادر إضافية

العيّنات

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

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