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

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

يُرجى الاطّلاع على فئات حجم النافذة.

مصادر إضافية

العيّنات

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

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