EduFlow

الوحدة 2 · التخطيط والتنسيق

أحجام الشاشات والـ safe areas وكود كل منصّة

19 دقراءة 3 دبيئة تجريب الكود

الدرس في لمحة

اجعل شاشات Trailhead تناسب أي هاتف باستخدام useWindowDimensions وهوامش الـ safe area، واكتب كودًا خاصًا بكل منصّة باستخدام Platform.select أو ملفات منفصلة لكل منصّة، فقط حيث تختلف المنصّتان فعلًا.

  1. 1قراءة حجم الشاشة بالطريقة الصحيحة
  2. 2الـ safe areas على المنصّتين
  3. 3حين تختلف المنصّتان
  4. 4شبكة تناسب الأجهزة اللوحية

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

تتراوح الهواتف التي يحملها مستخدموك بين أجهزة Android صغيرة بعرض نحو 360 نقطة وأجهزة iPhone كبيرة يتجاوز عرضها 430، إضافةً إلى الأجهزة اللوحية، والهواتف القابلة للطي التي يتغيّر عرضها في منتصف الجلسة، والوضع الأفقي. وفوق ذلك، الشاشة ليست مستطيلًا نظيفًا: هناك نتوء (notch) أو Dynamic Island، وزوايا مستديرة، ومؤشّر الشاشة الرئيسية، وعلى Android أشرطة الحالة والتنقّل التي يرسم تطبيقك الآن خلفها. ثلاث أدوات تجعل Trailhead يناسب كل ذلك.

قراءة حجم الشاشة بالطريقة الصحيحة

يُرجع useWindowDimensions() القيم width وheight وscale وfontScale للنافذة، ويعيد الـ render للـ component كلما تغيّرت:

TSX
import { useWindowDimensions } from 'react-native';

export function PhotoGrid({ photos }: { photos: string[] }) {
  const { width } = useWindowDimensions();
  const columns = width >= 700 ? 4 : 2;
  // …render photos in `columns` columns
}

ستجد كودًا قديمًا يستدعي Dimensions.get('window') على مستوى الوحدة (module). هذا يقرأ الحجم مرة واحدة، حين يُحمَّل الملف، ولا يقرؤه بعدها أبدًا: أدِر الهاتف، أو افتح التطبيق في وضع تقسيم الشاشة على iPad، أو افتح هاتفًا قابلًا للطي، وسيبقى التخطيط يستخدم العرض القديم. استخدم الـ hook داخل الـ components.

وفضّل الـ flex على القياسات حين تستطيع. معظم التخطيطات لا تحتاج إلى العرض أصلًا؛ استخدم useWindowDimensions حين تتغيّر البنية مع الحجم (عمودان يصبحان أربعة)، لا لحساب قيم بالبكسل يتولّاها الـ flexbox أصلًا.

الـ safe areas على المنصّتين

الـ safe area (المنطقة الآمنة) هي جزء الشاشة الذي لا تغطّيه فتحات العتاد ولا واجهة النظام. ترويسات التنقّل وأشرطة الـ tabs في Expo Router تحترمها أصلًا. أما الشاشات التي لا تحويها، مثل خريطة بملء الشاشة أو صفحة تعريفية، فيجب أن تتعامل معها بنفسها.

المكتبة المخصّصة لذلك هي react-native-safe-area-context. يتضمّنها Expo Router أصلًا ويوفّر الـ SafeAreaProvider في الجذر، فيمكنك استخدام الـ hook مباشرة:

src/app/track.tsx
import { StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function TrackScreen() {
  const insets = useSafeAreaInsets();

  return (
    <View style={styles.screen}>
      {/* the map fills the whole screen, edge to edge */}
      <View style={[styles.topBar, { top: insets.top + 8 }]}>
        <Text style={styles.timer}>01:24:10</Text>
      </View>
      <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 12 }]}>
        <Text>Stop and save</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1 },
  topBar: { position: 'absolute', left: 16, right: 16, alignItems: 'center' },
  timer: { fontSize: 20, fontWeight: '700' },
  bottomBar: { position: 'absolute', left: 0, right: 0, bottom: 0, paddingTop: 12, alignItems: 'center' },
});

يمكن للخريطة أن تُرسم تحت شريط الحالة، وهذا يبدو جميلًا، بينما تستقرّ عناصر التحكّم تحت المنطقة غير الآمنة بالضبط. للكائن insets القيم top وbottom وleft وright، فيعمل الوضع الأفقي أيضًا. وتصدّر المكتبة كذلك component باسم SafeAreaView يطبّق الحشو نيابةً عنك، وهو مفيد للشاشات البسيطة.

حين تختلف المنصّتان

يشارك React Native الكود بين iOS وAndroid، لكن المنصّتين ليستا متطابقتين، والتظاهر بأنهما كذلك ينتج تطبيقًا يبدو غريبًا على كلتيهما. يعطيك Platform نظام التشغيل الحالي:

TSX
import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  title: {
    fontSize: 17,
    fontWeight: Platform.OS === 'ios' ? '600' : '500',
  },
  card: Platform.select({
    ios: { boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)' },
    android: { elevation: 3 },
    default: {},
  }),
});

يُرجع Platform.select القيمة الخاصة بالمنصّة الحالية، ويعود إلى default إن لم يجدها. إنه الأداة المناسبة للفروق الصغيرة: وزن خط، أو ظلّ، أو hit slop.

وحين يختلف component بأكمله، قسّمه إلى ملفات. أنشئ DatePicker.ios.tsx وDatePicker.android.tsx، ثم استورد ./DatePicker دون امتداد؛ فيختار Metro الملف المناسب لكل منصّة. والأمر نفسه يعمل مع .native.tsx و.web.tsx إن عمل Trailhead يومًا على الويب.

أين تختلف المنصّتان فعلًا؟ في أعراف التنقّل (iOS يرجع بالسحب من الحافة اليسرى؛ وAndroid لديه إيماءة رجوع على مستوى النظام)، وفي أدوات الاختيار وحقول التاريخ، وفي الخطوط الافتراضية، والاهتزاز اللمسي (haptics)، ومسارات طلب الأذونات. وحيث لا تختلفان، كقائمة الرحلات والنماذج ومنطق العمل، شارك كل شيء.

شبكة تناسب الأجهزة اللوحية

لنجمع القطع معًا: يجب أن تعرض شبكة الصور أكبر عدد من الأعمدة يتّسع، مع حدّ أدنى لعرض البطاقة، ومسافات متّسقة، وحشو للشاشة. هذا حساب صغير يُنفَّذ في كل مرة يتغيّر فيها العرض، وهو بالضبط نوع الدوال النقية الذي يستحق أن تكتبه بشكل صحيح مرة واحدة. ستكتبه في التمرين أدناه.

التالي: القوائم. سيحمل Trailhead عمّا قليل مئات الرحلات، ورسمها كلها دفعة واحدة أسرع طريقة لجعل الهاتف يبدو بطيئًا.

أهم النقاط

  • useWindowDimensions() يعيد الـ render عند التدوير وتغيير الحجم؛ أما Dimensions.get() فلقطة واحدة تصبح قديمة.
  • الشاشات التي لا تحوي ترويسة تنقّل أو شريط tabs يجب أن تضيف حشوها بنفسها باستخدام هوامش الـ safe area من react-native-safe-area-context.
  • يرسم Android التطبيقات من الحافة إلى الحافة خلف أشرطة النظام، لذا تهمّ الهوامش على Android بقدر ما تهمّ على iOS.
  • استخدم Platform.select للفروق الصغيرة، وملفّي .ios.tsx / .android.tsx حين يختلف component بأكمله.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

تحسب شبكة الصور في Trailhead عدد أعمدتها مرة واحدة باستخدام Dimensions.get('window').width في أعلى الملف. ما الذي ينكسر؟

السؤال 2 من 3

خريطة Trailhead بملء الشاشة ليس لها ترويسة، وأزرارها العلوية تقع تحت الـ Dynamic Island في iPhone. ما الإصلاح الصحيح؟

السؤال 3 من 3

يحتاج منتقي التاريخ في Trailhead إلى components مختلفة كليًا على iOS وAndroid، بينما بقية النموذج مشتركة. ما البنية الأنظف؟

تدريب

بيئة تجريب الكود

اجعل شبكة الصور تناسب أي شاشة

تستدعي شبكة الصور في Trailhead الدالة gridLayout(width) بالقيمة القادمة من useWindowDimensions(). أصلح gridLayout(screenWidth, options) لتُرجع { columns, cardWidth } بحيث:

  • للشاشة حشو بمقدار padding على اليسار واليمين، وبين البطاقات مسافة gap نقطة (دون مسافة عند الحافتين الخارجيتين)،
  • columns هو أكبر عدد من البطاقات يكون عرض كلٍّ منها minCardWidth على الأقل، ولا يقلّ أبدًا عن 1،
  • cardWidth هو العرض الذي تحصل عليه كل بطاقة حين تُقسَم المساحة المتبقية بالتساوي، مقرَّبًا إلى الأسفل إلى نقطة كاملة.

القيم الافتراضية: minCardWidth = 160، gap = 12، padding = 16. مثلًا، iPhone بعرض 390 نقطة يعطي { columns: 2, cardWidth: 173 }.

// Work out how many photo cards fit across the screen, and how wide each one is.
function gridLayout(screenWidth, { minCardWidth = 160, gap = 12, padding = 16 } = {}) {
  const columns = Math.floor(screenWidth / minCardWidth);
  const cardWidth = screenWidth / columns;
  return { columns, cardWidth };
}

console.log(gridLayout(390)); // iPhone in portrait
console.log(gridLayout(820)); // iPad in portrait
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • هاتف بعرض 390 نقطة يحصل على عمودين بعرض 173
  • هاتف بعرض 320 نقطة يحصل على عمود واحد بعرض 288
  • جهاز لوحي بعرض 820 نقطة يحصل على 4 أعمدة بعرض 188
  • تُحترَم القيم المخصّصة لـ minCardWidth وgap وpadding
  • الشاشة الأضيق من بطاقة واحدة تحصل مع ذلك على عمود واحد