اذهب إلى المحتوى

البحث في الموقع

المحتوى عن 'android'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

تاريخ الانضمام

  • بداية

    نهاية


المجموعة


النبذة الشخصية

تم العثور على 15 نتائج

  1. Android هو نظام تشغيل مفتوح المصدر مبني على نواة لينكس مع إضافة بعض التعديلات عليها ليعمل النظام على الهواتف المحمولة والحواسيب اللوحية، وغيرهما من الأجهزة الذكية المختلفة، ويتم تطوير إصدارات النظام بواسطة شركة جوجل. الطبقات الرئيسية المكونة لنظام أندرويد الصورة التالية توضح الهيكل الداخلي للأندرويد والطبقات الرئيسية المكونة لنظام التشغيل. ينقسم نظام التشغيل إلى خمس طبقات وظيفتها كالآتي: طبقة التطبيقات Application Layer (الطبقة العلوية) وهي الطبقة التي نتعامل معها دائمًا كمستخدمين لنظام التشغيل، فهي تحتوي على تطبيقات النظام والتطبيقات التي نقوم بتحميلها من المتجر -وهي الطبقة التي سيعمل فيها تطبيقك في آخر هذا الدرس-، أمثلة على ذلك تطبيق الاتصال وتطبيق البريد الإلكتروني وتطبيق المتصفح. الطبقة الخاصة ببيئة عمل التطبيقات Application Framework Layer تحتوي تلك الطبقة على مكتبات بُنيت بلغة الجافا خصّيصًا لنظام تشغيل أندرويد وتوفر هذه المكتبات طرق الوصول إلى الموارد الخاصة بالهاتف مما يجعل تطوير التطبيقات أسهل -وهي الطبقة التي سنتعامل معها كمطوري تطبيقات للأندرويد- وأمثلة على ذلك: View System: وهي مكتبة تحتوي على العناصر اللازمة لإنشاء واجهة المستخدم مثل TextViews و Buttons و Checkboxes و غيرها من العناصر الخاصة بواجهة المستخدم. Notification Manager: تحتوي هذه المكتبة على العناصر اللازمة لإنشاء و إرسال الإشعارات للمستخدم الخاصة بتطبيقك. Telephony Manager: وهي المكتبة المسؤولة عن استقبال و إرسال المكالمات الهاتفية من داخل تطبيقك. Location Manager: وهي المكتبة المسؤولة عن تحديد المواقع باستخدام GPS Sensors المتواجدة بالهاتف. أسفل هذه الطبقة سنجد طبقة خاصة تنقسم إلى جزئين: طبقة المكتبات المطورة بلغة C و ++C (المعروفة باسم Libraries Layer) وتحتوي هذه الطبقة على مكتبات مكتوبة بلغة C لقدرة لغة C على القيام بالمهام القوية بشكل كفء دون إهدار لموارد النظام. أمثلة على ذلك: SQLite: تستخدم للتعامل مع قواعد البيانات. OpenGL|ES: تستخدم للتعامل مع الرسوميات ثنائية و ثلاثية الأبعاد و تستخدم بشكل أكبر مع ثلاثية الأبعاد. FreeType: تستخدم للتعامل مع أنواع الخطوط المختلفة. Media FrameWork: تستخدم للتعامل مع الصيغ المختلفة لملفات الفيديو. Android Runtime و تحتوى هذه الطبقة على مجموعة من المكتبات والتي تجعل المطور قادر على برمجة التطبيقات باستخدام لغة الجافا. كما تحتوى على الآلة الافتراضية (Virtual Machine) المسؤولة عن تشغيل التطبيقات والتي تم تطويرها لكي تعمل على الهواتف المحمولة وتتميز باستهلاكها القليل للموارد من الذاكرة العشوائية والمعالج وبطارية الهاتف وتسمى بـ Dalvik Virtual machine. ويعمل كل تطبيق داخل نسخة خاصة به من الآلة الافتراضية مما يميزها بالأمان فالتطبيق لا يستطيع أن يرى باقي التطبيقات ولا يستطيع الوصول إلى بيانات من داخل الهاتف دون علم المستخدم ومنحه الصلاحيات اللازمة لذلك - تلك الصلاحيات التي يتم منحها للتطبيق عند تحميله من المتجر- لذلك ينصح بالتدقيق فيما يحتاجه التطبيق من صلاحيات وألا تقوم بتحميل تطبيقات ذات صلاحيات زائدة عن حاجة التطبيق لكي يعمل ، وصُممت الآلة الافتراضية بطريقة تجعل أكثر من نسخة من الآلة الافتراضية تعمل معًا بسلاسة وذلك لكي يصبح المستخدم قادراً على تشغيل واستخدام أكثر من تطبيق معًا دون الشعور باختلاف في الأداء. طبقة النواة (Kernel) وهي الطبقة المسؤولة عن التعامل مع العتاد المختلف للهواتف فتحتوي على التعريفات الأساسية الخاصة بهذا العتاد كما توفر طريقة أبسط للطبقة العلوية للوصول لمميزات العتاد وهي نفس نواة لينكس. تعتبر كل طبقة هي حلقة الوصل بينها وبين ما فوقها أو تحتها من طبقات، وكلما أتجهنا للأسفل كلما استُخدمت لغات وطرق أقرب في تعاملها مع العتاد كلغة C أو الأسمبلي مثلاً وكلما صعدنا للأعلى كلما استُخدمت لغات وطرق أسهل للمستخدم وتقدم خدمات غنية له كالجافا مثلاً. إصدارات أندرويد قبل البدء بتطوير التطبيقات ينبغي علينا معرفة ما هي إصدارات أندرويد، سأقوم سريعًا بذكر كافة إصدارات أندرويد المختلفة ورقم الإصدار ورقم الـ API الخاص بها. Cupcake 1.5 API 3 Dount 1.6 API 4 Eclair 2.0 API 5 Froyo 2.2 API 8 Gingerbread 2.3.3 API 10 Honeycomb 3.0 API 11 Ice Cream sandwich 4.0 API 14 Jellybean 4.1 API 16 Kitkat 4.4 API 19 Lollipop 5.0 API 22 Marshmallow 6.0 API 23 بعد أن تعرفنا على إصدارات أندرويد تبقى علينا معرفة عدد مستخدمي كل إصدار منهم لأنه ليس بالضرورة أن يكون أحدث إصدار هو الأفضل لتطوير التطبيقات من أجله، فعدد المستخدمين لهذا الإصدار أحد العوامل المحددة لذلك. تقوم جوجل شهرياً بنشر إحصائيات عن عدد مستخدمي كل إصدار من إصدارات أندرويد ويمكنك معرفتها عن طريق الرسم البياني التالي. لكل إصدار من إصدارات أندرويد مزايا خاصة أُضيفت له لم تكن متوافرة في الإصدارات الأقدم، ولكي تستطيع بناء هذه المزايا في تطبيقك ينبغي اختيار الإصدار الأدنى المناسب لمزايا تطبيقك لأنك تستطيع دعم المزايا الخاصة بالإصدار الأدنى فقط، لذا كلما قمنا بدعم إصدارات أقدم كلما فقدنا المزايا الحديثة وفي المقابل نحصل على عدد مستخدمين أكثر وإذا قمنا بدعم أحدث الإصدارات سوف نحصل على مزايا أكثر وعدد مستخدمين أقل. لذا سنختار الحد الأدنى الذي سنحتاجه من المزايا دون خسارة عدد المستخدمين، وإصدار Jellybean يتيح لك مزايا جيدة واستهداف عدد مستخدمين أكبر لأن تطبيقك سيعمل على كل من Jellybean و kitkat و lollipop و marshmallow وما سيأتي بعد ذلك من إصدارات أحدث وهو كما يتضح في الرسم البياني السابق أكثر من 90% من مستخدمي أندرويد، ولكنه لن يعمل على أي هاتف يعمل بإصدار أقدم من Jellybean. المتطلبات للبدء ببرمجة تطبيقات أندرويد كل ما تحتاجه هو حاسوب يعمل بأي نظام من أنظمة التشغيل الرئيسية -ويندوز أو لينكس أو ماك- لتبدأ معنا هذه الدروس. لا يوجد حاجة لمعرفة سابقة بلغة برمجة فسنتعلم معاً ما نحتاج إليه. هذه الدروس موجهة بشكل أساسي للمبتدئين. البرامج التي سنعمل عليها خلال الدروس القادمة: JDK Android Studio قم بتحميل هذه البرامج وبتثبيتها على جهازك. ملاحظة: ينبغي تثبيت JDK أولاً قبل البدء في تثبيت Android Studio حيث أنه يحتاج إليه عند التثبيت. إنشاء مشروع جديد الآن بعد الانتهاء من تثبيت البرامج اللازمة لتطوير تطبيقات أندرويد دعنا نُنشئ مشروعنا الأول وهو مشروع "أهلاً بالعالم Hello World". قم بفتح برنامج Android Studio واختر Start a new android studio project من القائمة. قم باختيار اسم التطبيق الخاص بك ثم اكتب نطاق موقعك – إن وجد – أو قم بكتابة الاسم كما هو موجود في الصورة وسيتم شرح فائدة هذا الاسم لاحقاً. اختر تطوير التطبيق من أجل Phone and Tablet ثم اختر إصدار Jellybean كإصدار أدنى كما ذكرنا سابقًا. اختر Blank Activity ثم أضغط Next. سنبقي هذه الخانات على حالتها الافتراضية حيث يمكننا تغييرها فيما بعد وأضغط Finish. تم إنشاء أول مشروع لك بنجاح. ملفات ومجلدات المشروع دعنا الآن نستعرض بيئة التطوير وما توفره لنا كمطورين من مزايا. على اليسار هناك ثلاثة ملفات / مُجلّدات رئيسية تكونت تلقائياً بداخل app وهي: manifests java res مجلد manifest والذي يحتوي على ملف AndroidManifest.xml ويعتبر هذا الملف مهم جداً لنظام أندرويد لما يقدمه من معلومات أساسية للنظام قبل أن يقوم بتشغيله، أبرز هذه المعلومات: يحتوي على اسم الحزمة Package Name: وهو اسم مميز للتطبيق لتمييزه عن باقي التطبيقات المتواجدة على الهاتف ، فمثلاً إن كان اسم تطبيقك Gallery و هناك تطبيق آخر على الهاتف يسمى Gallery كيف يستطيع النظام التفرقة بين صلاحيات كل منهما و معلوماتهما الخاصة؟ يقوم النظام بتمييزهما عن طريق Package Name لذا فهو يعتبر اسمًا مُميّزًا لتطبيقك لا ينبغي أن يتشابه مع Package Name لأي تطبيق آخر ، من أجل ذلك وضعت قواعد لتسمية الـ Package أهمها هو أن تجعل تطبيقك على هيئة اسم نطاق لموقعك - إن وجد – فمثلا نجد شركة Google عند تسمية حزم تطبيقاتها تستخدم com.google.appName فيكون لها اسم حزمة Package Name فريد لا يتكرر. ملحوظة: يتم تحديد Package Name عند إنشاء التطبيق في الخانة الخاصة بالـ Company Domain. يقوم بتعريف المكونات الأساسية التي يتكون منها التطبيق الخاص بك: سوف نتعرف على هذه المكونات في الدرس التالي، ويتكون التطبيق من أحد هذه المكونات أو أكثر على حسب حاجة التطبيق. يحتوي الإصدار الأدنى الذي يعمل التطبيق معه وهو ما قمنا بتحديده مسبقا عند إنشاء للمشروع. يحتوي على الصلاحيات التي سيعطيها النظام للتطبيق –إن وجدت-. يحتوي على الصلاحيات التي يجب على التطبيقات الأخرى أن تطلبها إن أرادت أن تتبادل معلومات مع تطبيقك. لذا يعتبر هذا الملف من الملفات الأساسية التي يعتمد عليها النظام في تحديد كيفية التعامل مع التطبيق وإذا لم يتم تحديد كل شيء بشكل صحيح فذلك يعرض تطبيقك إلى ألا يعمل. مجلد java يختص هذا المُجلّد بالشيفرة التى سنكتبها للتطبيق و هي الشيفرة الذى يحدد وظيفة التطبيق و استجابة الواجهة و تنفيذها للأوامر. مجلد res و يهتم هذا المُجلّد بكل الموارد التي يتعامل معها التطبيق من صور و عناصر مكونة لواجهة المستخدم و عناصر مكونة للقوائم ويتكون من مجموعة من المجلّدات المرتبة كالآتي: drawable ويخص هذا المُجلد كافة الصور التي سيتم استخدامها في التطبيق. layout ويخص هذا المجلد بالتصميم الخاص بواجهة المستخدم و ما تحتويه من عناصر مختلفة. menu ويخص هذا المجلد بالقوائم و ما تحتويه من عناصر. mipmap ويخص هذا المجلد بالأيقونة الخاصة بالتطبيق فقط. values ويحتوى هذا المجلد على ملفات أخرى لكل منها وظيفة مختلفة و لكنها تشترك في فكرة عامة واحدة وهي جعل التطبيق أكثر مرونة لدعم دقة الشاشات المختلفة و الأحجام المختلفة ودعم اللغات المختلفة كما سنرى لاحقا في الدروس القادمة. المحاكي Simulator كما تتميز بيئة تطوير Android Studio بأنها بيئة تطوير متكاملة وتحتوي على ما يحتاجه المطور من أدوات لصنع تطبيق أندرويد فكما رأينا فهي تحتوي على مستعرض للمشروع وملفاته وتحتوي على المترجم الخاص والذي يحول الشيفرة إلى صيغته التنفيذية والتي تعمل على الهواتف وأيضاً تحتوي على محاكي للهواتف والحواسيب اللوحية ونستطيع تجربته عن طريق الضغط على (AVD (Android Virtual Device من داخل Android Studio في شريط القوائم وسنجد أنه تلقائياُ لديه هاتف جاهز للعمل. كما يمكنك أن تقوم بصنع محاكي آخر عن طريق الضغط على Create Virtual Device. الآن قم بتشغيل المحاكي و انتظر حتى يعمل كالتالي. قد يأخذ المحاكي بعض الوقت خاصة عند تشغيله أول مرة لذا ينصح دائما أن تقوم بتشغيل المحاكي قبل أن تقوم بتجربة التطبيق بفترة كافية. لتجربة التطبيق الذي قمنا بإنشائه على المحاكي من داخل android studio اضغط على الأيقونة Run ثم انتظر حتى تظهر أمامك شاشة يظهر فيها اسم المحاكي. ثم اختر المحاكي وأضغط ok. ستجد التطبيق يعمل الآن على المحاكي. ملحوظة: عند إنشاء أي مشروع داخل بيئة عمل Android Studio يقوم تلقائياً بإنشاء مشروع يعرض فقط كلمة !Hello World أمام المستخدم وهو الذي شاهدناه في الصورة السابقة حيث أننا لم نقوم بتغيير أي شيء في المشروع الأصلي. بهذا نكون قد وصلنا إلى نهاية أول دروسنا من هذه السلسة وإلى لقاء قريب بإذن الله، في انتظار تجربتكم وما مررتم به من مشاكل إن وجدت.
  2. العناصر الأساسية في تطبيقات أندرويد تتكون تطبيقات أندرويد من أربعة عناصر أساسية لكل منها دورها الخاص الذي تستطيع القيام به، ويتم استخدامها بناءً على الغرض أو المهمة المطلوب تنفيذها وهي: النشاطات Activities يُمثل واجهة واحدة والتي تظهر أمام المستخدم ويتفاعل معها، وقد يتكون التطبيق من عدة واجهات مختلفة وبالتالي عدة Activities مختلفة، فمثلًا تطبيق البريد الإلكتروني لديه واجهة خاصة بعرض البريد الجديد وعند قراءة محتوى رسالة محددة تظهر لك في واجهة أخرى مخصصة لذلك وإذا أردت إرسال رسالة بريد جديدة يظهر لك واجهة جديدة لقيام بهذه المهمة. فأغلب تطبيقات أندرويد تتكون من عدة نشاطات Activities ولكن عند فتح التطبيق هناك واجهة واحدة دائمًا ما تكون هي الواجهة الرئيسية والتي تُعرض أمام المستخدم وينتقل من خلالها إلى باقي النشاطات Activities الخاصة بالتطبيق. الخدمات Services يُستخدم هذا العنصر بشكل أساسي عند القيام بعمليات طويلة ويتم تنفيذها في الخلفية دون تعطيل الواجهة الرئيسية أو تعامل المستخدم مع التطبيق، ومثال على ذلك عند تشغيل تطبيق المتصفح وتحميل ملف من أحد المواقع فيتم تنفيذ هذه المهمة في الخلفية ويتفاعل المستخدم مع المتصفح في ذات الوقت ولا ينتظر انتهاء التحميل، وذلك لأن هذه المهمة تمت من خلال العنصر Service. مستقبلات البث Broadcast Receivers هي حلقة الاتصال بين التطبيق ونظام التشغيل. ويستخدم هذا العنصر لاستقبال الرسائل التي يبُثها نظام التشغيل أو تطبيق آخر، فمثلًا عندما يستقبل نظام التشغيل مكالمة هاتفية يقوم ببث رسالة لكل التطبيقات المتواجدة على الهاتف لإعلامهم بوجود مكالمة تحتاج إلى التطبيق الخاص بالاتصال والذي يستطيع التعامل معها وعرض بيناتها أمام المستخدم ويكون في تطبيق الاتصال هذا العنصر لاستقبال الرسالة من نظام التشغيل والتعامل معها، أو عندما ينتهي تطبيق من تحميل صورة يُرسل لباقي لتطبيقات رسالة أن هناك صوة جديدة على الهاتف يمكن للتطبيق المسؤول عنها التعامل معها. لذا فهذا العنصر هو المسؤول عن استقبال هذا النوع من الرسائل واتخاذ القرار المناسب. مزودو المحتوى Content Providers يوفر هذا العنصر طريقة لمشاركة البيانات بين التطبيقات المختلفة، مثال على ذلك قاعدة البيانات الخاصة بجهات الاتصال على هاتفك نجد أن هناك بعض التطبيقات التي تستخدمها في تقديم خدماتها. النشاطات Activities نبدأ أولًا بالنشاطات Activities حيث لا يخلو تطبيق من واجهة للمستخدم، لصنع نشاط في التطبيق يتم ذلك عن طريق كتابة صنف جديد Class يرث من صنف آخر اسمه Activity، وهذا الصنف هو الأب دائمًا عند عمل نشاط جديدة ويحتوي على التوابع Methods الخاصة بالنشاط والمكوّنة لها. public class MainActivity extends Activity { } إن كانت لديك خبرة سابقة في التعامل مع أي لغة برمجة فمن المؤكد أنك رأيت الدالة ()main والتي تُمثل نقطة البداية للبرنامج، ولكن في أندرويد هناك نشاط رئيسي يُمثل نقطة البداية للتطبيق ولكل نشاط دورة حياة خاصة به، وتتكون من مجموعة من التوابع methods يتم استدعاؤها لحظة إنشاء النّشاط ومجموعة من التوابع التي يتم استدعاؤها لحظة إزالة النّشاط ونجدها في الصورة التالية. دورة حياة النشاط ()onCreate: ويتم استدعاؤه عند إنشاء النشاط أول مرة، ويتم بداخلها ربط الواجهة بالشيفرة التي تتحكم في التطبيق. ()onStart: ويتم استدعاؤه عندما تبدأ الواجهة في الظهور وبعد الانتهاء من ()onCreate. ()onResume: ويتم استدعاؤه عندما تظهر الواجهة الخاصة بالـ Activity ويتفاعل معها المستخدم. ()onPause: ويتم استدعاؤه عندما تبدأ الواجهة في الاختفاء أو تظهر بشكل جزئي ولا يمكن للمستخدم التفاعل معها لوجود شيء ما يحجبها وعند إزالة ما يحجبها يتم استدعاء ()onResume مرة أخرى. ()onStop: ويتم استدعاؤه عندما تختفي الواجهة من أمام المستخدم نتيجة للانتقال إلى واجهة أخرى داخل نفس التطبيق أو لغلق التطبيق والانتقال لتطبيق آخر. ()onRestart: ويتم استدعاؤه عند الرجوع مرة أخرى إلى نفس الواجهة ثم يتم استدعاء ()onStart. ()onDestroy: ويتم استدعاؤه قبل تدمير الواجهة وإزالتها من الذاكرة ويتم ذلك عندما يحتاج نظام التشغيل إلى المزيد من المساحة في الذاكرة أو إزالتها برمجيًا من الذاكرة. مثال 1 الآن قم بفتح Android Studio وسنقوم بعمل تطبيق جديد اسمه "Activity Life Cycle" حتى نرى كيف ينادي النظام التّوابع الخاصة بالنّشاط. سنختار Empty Activity وندع باقي الاختيارات كما هي. بعد الانتهاء ستجد في المجلد java صنف Class يدعى MainActivity ويرث من Activity (قد تجد الصنف يرث من AppCompatActivity يمكنك تغييرها إلى Activity أو دعها كما هي فلا يوجد حاليًا فرق بينها). ستجد داخل الصنف التابع ()onCreate. @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } وستجد بداخلها أنها تستدعي ;(setContentView(R.layout.activity_main وتمرر لها R.layout.activity_main، والحرف R يعني المجلد res، و Layout هو المجلد المتواجد داخل res بنفس الاسم، وبداخله نجد الملف activity_main.xml. ويقوم هذا التابع بربط الواجهة التي نقوم بصنعها في ملف XML بالشيفرة المتواجدة في ملف الجافا للتحكم بالعناصر المتواجدة في الواجهة. سنضع هذا السطر بداخل ()onCreate. Toast.makeText(this,"onCreate Method",Toast.LENGTH_SHORT).show(); ومن خلال هذا السطر نقوم بإظهار رسالة للمستخدم لمدة محددة كما في الصورة. والآن نقوم بنفس الشيء لباقي التوابع: package apps.noby.activitylifecycle; import android.app.Activity; import android.os.Bundle; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toast.makeText(this,"onCreate Method",Toast.LENGTH_SHORT).show(); } @Override protected void onStart() { super.onStart(); Toast.makeText(this,"onStart Method",Toast.LENGTH_SHORT).show(); } @Override protected void onResume() { super.onResume(); Toast.makeText(this,"onResume Method",Toast.LENGTH_SHORT).show(); } @Override protected void onStop() { super.onStop(); Toast.makeText(this,"onStop Method",Toast.LENGTH_SHORT).show(); } @Override protected void onPause() { super.onPause(); Toast.makeText(this,"onPause Method",Toast.LENGTH_SHORT).show(); } @Override protected void onDestroy() { super.onDestroy(); Toast.makeText(this,"onDestroy Method",Toast.LENGTH_SHORT).show(); } @Override protected void onRestart() { super.onRestart(); Toast.makeText(this,"onRestart Method",Toast.LENGTH_SHORT).show(); } } ثم نقوم بتشغيله على المحاكي لنرى التطبيق وهو يعمل. ملاحظات التابع ()onCreate هو التابع الوحيد الذي نقوم بداخله بتعريف واجهة المستخدم من داخل المجلد layout باستخدام التابع ()setContentView. لا يشترط عند كتابة التوابع كتابتها بترتيب استدعائها. يجب أن يحتوي النّشاط على التابع ()onCreate وإلا لن تعمل، وباقي التوابع نستخدم منها فقط ما نحتاجه لأداء مهمة محددة في وقت محدد، فمثلًا إذا كان التطبيق يستخدم الكاميرا وقام المستخدم بغلق التطبيق أو الانتقال إلى تطبيق أخر فيجب عند هذه الحالة قبل اختفاء النّشاط غلق الكاميرا حتى لا يتم إهدار الموارد دون استخدام، فأنسب مكان لذلك هو التابع ()onStop فهو الذي يُعبر عن هذه الحالة . عند صنع نشاط جديد يجب تعريفه في ملف AndroidManifest.xml كما بالشكل، وإذا لم يتم تعريفه فلن تعمل ولن يستطيع نظام التشغيل تشغيلها. ... <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> ... يُستخدم ملف AndroidManifest.xml لتعريف نظام التشغيل بالمكونات الأساسية التي يتكون منها التطبيق، داخل الوسم <application> سنجد وسمًا آخر يدعى <activity> وهو الوسم المستخدم لتعريف نظام التشغيل أن التطبيق يحتوي على Activity ولدى هذا الوسم الخاصية name لمعرفة ملف جافا الخاص بهذا الـ Activity. وكما ذكرنا أن تطبيقات أندرويد تتكون من Activity رئيسية تمثل نقطة البداية للتطبيق لذا لتعريف النظام بأن هذا النّشاط هو الرئيسي نستخدم الوسم <intent-filter> وبداخله يتم تعريف Action من النوع MAIN وتعريف Category من النوع LAUNCHER وإذا لم يتم تعريفهما معًا فلن تظهر الأيقونة الخاصة بالتطبيق مع باقي التطبيقات في قائمة التطبيقات الرئيسية. مثال 2 سنقوم بصنع تطبيق كما في الصورة التالية يحتوي على نص وزر عند الضغط عليه يتغير النص إلى كلمة أخرى من اختيارنا. أولًا نقوم بصنع الواجهة الخاصة بالتطبيق في ملف activity_main.xml: <?xml version=”1.0” encoding=”utf-8”?> <LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android” android:layout_width=”match_parent” android:layout_height=”match_parent” android:orientation=”vertical”> <TextView android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:text=”Hello World!” android:id=”@+id/txt”/> <Button android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:text=”Change Text” android:id=”@+id/chngbtn”/> </LinearLayout> لا يوجد اختلاف بينه وبين ما قمنا بعمله سابقًا، واستخدمنا خاصية id للتحكم في هذا العنصر من شيفرة الجافا وأعطينا لكل عنصر Id مميز. ثانيًا الاستجابة للزر برمجيًا وتغيير النص، ولكن يجب أولًا أن نصنع متغير ونربطه بالزر المصنوع في xml، ونقوم بذلك كما في الشكل التالي: Button btn; ... btn = (Button) findViewById(R.Id.chngbtn); قمنا بتعريف متغير من الصنف Button وهو صنف موجود في المكتبات الخاصة بأندرويد، وقمنا بربطه باستخدام التابع ()findViewById وتمرير له R.Id.chngbtn وتعني من المجلد res ستجد عنصر له Id قيمته chngbtn. ويقوم هذا التابع بالبحث عن العناصر بمعرفة Id الخاص بها وإرجاعها، وقمنا باستخدام خاصية التحويل Casting وذلك لأن التابع يرجع عنصرًا من النوع View وهو الأب للعنصر Button لذا نقوم بتحويله إلى الصنف المحدد عن طريق (Button). وبالمثل نقوم بربط TextView في شيفرة الجافا. والآن لجعل الزر يفعل شيئًا عند الضغط عليه نقوم باستخدام Listeners والتي تقوم بدورها بانتظار أن يضغط المستخدم على الزر حتى تقوم بوظيفة ما. btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Do Something here } }); باستخدام المتغير btn نقوم باستدعاء التابع setOnClickListener وتمرير له ()new View.OnClickListener وهو ما يدعى بالصنف المجهول Anonymous class، وبداخله نجد الدالة onClick والتي تُستدعى عند الضغط على الزر ويتم تنفيذ الأوامر التي بداخلها. سنقوم الآن بتغيير الكلمة عند الضغط على الزر من !Hello World إلى Hello Android Developer فداخل onClick نكتب: String str = "Hello Android Developer"; tv.setText(str); قمنا بتعريف متغير من النوع String وتخزين النص بداخله، وباستخدام المتغير tv نستدعي التابع setText وهو المسؤول عن كتابة نص في TextView برمجيًا ومساوٍ للخاصية android:text في xml ونمرر له النص المراد كتابته. لتُصبح الشيفرة النهائية هي: package apps.noby.controlapps; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends Activity { Button btn; TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn =(Button) findViewById(R.id.chngbtn); tv = (TextView) findViewById(R.id.txt); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String str = "Hello Android Developer"; tv.setText(str); } }); } } عند تشغيل التطبيق على المحاكي والضغط على الزر نجد أن التطبيق يعمل بالشكل المطلوب. ملاحظات يتم تعريف نوع المتغيرات خارج التابع onCreate. تستخدم import عندما يكون هناك صنف متواجد في إحدى المكتبات ونريد تضمينه داخل تطبيقنا فنقوم بكتابة بجانب الأمر import ومكان تواجد هذا الصنف. يجب ذكر اسم الحزمة في بداية الشيفرة الخاصة بالنّشاط. يمكن تمرير النص للتابع setText في سطر واحد مثل: tv.setText("Hello Android Developer"); بدلًا من تخزين قيمة النص في متغير ثم تمرير هذا المتغير. مثال 3 سنضيف في هذا المثال EditText ليستطيع المستخدم كتابة نص ثم سنقوم بعرضه له بدلًا من عرض نص محدد مسبقًا. أولًا نبدأ بصنع الواجهة ولن تختلف عن واجهة المثال السابق إلا بزيادة عنصر من النوع EditText: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:id="@+id/txt"/> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Write your Name here" android:id="@+id/edt"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Change Text" android:id="@+id/chngbtn"/> </LinearLayout> الآن للتحكم فيما يكتبه المستخدم بداخل EditText برمجيًا نعرف متغيرًا جديدًا داخل ملف MainActivity.java ونربطه بالعنصر الذي قمنا بتعريف في الواجهة. ولتحديد ما سيتم كتابته عند الضغط على الزر: String str = et.getText().toString(); tv.setText("Hello " + str + "!"); باستخدام المتغير get نستدعي التابع getText وهو المسؤول عن إعادة البيانات التي قام المستخدم بكتابتها في العنصر EditText ثم نستدعي التابع toString لتحويلها إلى نص و تخزينه بداخل المتغير str ، ثم نمرر هذا المتغير إلى التابع setText كما سبق، تستخدم "+" للربط بين نصين ففي هذا المثال نريد الربط بين كلمة Hello والقيمة التي أدخلها المستخدم. package apps.noby.controlapps; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; public class MainActivity extends Activity { Button btn; TextView tv; EditText et; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (Button) findViewById(R.id.chngbtn); tv = (TextView) findViewById(R.id.txt); et = (EditText) findViewById(R.id.edt); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String str = et.getText().toString(); tv.setText("Hello " + str + "!"); } }); } } ثم نقوم بتجربة التطبيق على المحاكي. مثال 4 سنقوم بصنع تطبيق يظهر صورة عند الضغط على الزر ثم يخفيها عند الضغط عليه مرة أخرى. أولًا نبدأ بصنع واجهة المستخدم: <?xml version="1.0" encoding="utf-8"?> <!-- activit_main.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show" android:onClick="showImage" android:id="@+id/show"/> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/and_img" android:visibility="invisible" android:id="@+id/ic"/> </LinearLayout> قمنا بتغيير أماكن ظهور العناصر إلى المنتصف عن طريق الخاصية: android:gravity="center" وجعلنا الصورة عند بداية استخدام التطبيق غير ظاهرة باستخدام الخاصية: android:visibility="invisible" واستخدمنا خاصية جديدة للزر تدعى: android:onClick="showImage" وتستخدم كطريقة أخرى لصنع Listener ينتظر ضغط المستخدم على الزر ولكن عن طريق xml. ثانيًا للتحكم بهذه الواجهة نقوم في ملف MainActivity.java بربط ImageView بمتغير كما سبق. ولظهور واختفاء الصورة عند الضغط على الزر: public void showImage(View view){ if(img.getVisibility() == View.VISIBLE){ img.setVisibility(View.INVISIBLE); showBtn.setText("Show"); } else{ img.setVisibility(View.VISIBLE); showBtn.setText("Hide"); } } نستخدم هذه الدالة والتي يجب أن تكون كالتالي: الوصول لها Public ولا ترجع أية قيمة (يعني تُرجع void) الاسم الخاص بها نفس الاسم الذي كتبناه سابقًا للخاصية onClick. تمرير للدالة عنصر من النوع View. ثم نقوم بكتابة ما سيحدث عند الضغط على الزر كما كنا نفعل سابقًا. ونقوم داخل الدالة بالتحقق إذا ما كان العنصر ظاهرًا باستدعاء التابع ()getVisibility باستخدام المتغير img، فإن كان ظاهرًا نقوم بإخفائه باستدعاء التابع ()setVisibility وتمرير له الثابت View.INVISIBLE، ثم نغير النص المكتوب على الزر. أما إذا كانت الصورة غير ظاهرة نقوم بتمرير الثابت View.VISIBLE للتابع ()setVisibility، ثم تغيير النص المكتوب على الزر أيضًا. package apps.noby.controlapps; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.ImageView; public class MainActivity extends Activity { Button showBtn; ImageView img; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); showBtn = (Button) findViewById(R.id.show); img =(ImageView) findViewById(R.id.ic); } public void showImage(View view){ if(img.getVisibility() == View.VISIBLE){ img.setVisibility(View.INVISIBLE); showBtn.setText("Show"); } else{ img.setVisibility(View.VISIBLE); showBtn.setText("Hide"); } } } ثم نقوم بتجربة التطبيق على المحاكي. بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  3. ملاحظة: يمكن تحميل الملفات المصدرية لكامل هذه السلسلة عبر حساب أكاديمية حسوب على Github، يمكن أيضا تحميل ملف APK لتجريب اللعبة على أجهزة Android. استقبال مدخلات شاشة اللمساستقبال المدخلات من شاشة اللمس تختلف عن استقبال مدخلات الفأرة أو لوحة المفاتيح. فأنت هنا تتعامل مع شاشة تستقبل عدة لمسات، وتتعامل مع أحداث مثل وضع الإصبع وتحريكه ورفعه، أو وضع أصبعين وتقريبهما أو إبعادهما عن بعضهما. كل هذه الأنماط من الإدخال سيكون عليك التعرف عليها وتنفيذ ما يناسبها من أوامر. لنبدأ أولا مع مدخلات عناصر واجهة المستخدم مثل الأزرار. ما الذي يجب علينا فعله لاستقبال المدخلات على هذه العناصر؟ لا شيء! فهذا الأمر يقوم به محرك Unity تلقائيا. أي أنك عندما تلمس زرا على الشاشة سيتعرف التطبيق على هذا الأمر كأنك ضغطت على الزر وينفذ الأمر أو الأوامر المرتبطة به. لننتقل الآن للمدخلات الخاصة باللعبة. تذكر أننا قمنا بكتابة 3 بريمجات لاستقبال مدخلات الفأرة، وسنحتاج الآن لكتابة 3 مقابلها لاستقبال مدخلات شاشة اللمس. هذه البريمجات الثلاث هي: CameraMouseInput والذي أضفناه لقوالب عناصر الخلفية والأرضية من أجل تحريك الكاميرا والتحكم بتقريبها وإبعادها.LauncherMouseInput والذي أضفناه على قالب المقلاع بحيث يستقبل مدخلات اللاعب على المقذوفات ويحدد اتجاه التصويب وشدته ويطلق المقذوف حين إفلاته.SpecialAttackMouseInput والذي أضفناه على قوالب المقذوفات من أجل تنفيذ الهجوم الخاص عند الضغط على زر الفأرة بعد الإطلاق.الموضوع ببساطة شديدة أننا مقابل كل بريمج إدخال من الفأرة سنكتب بريمج إدخال من شاشة اللمس ونضيفه على نفس القالب. القاسم المشترك بين قوالب استقبال مدخلات اللمس أنها ستقوم بالتحقق من بيئة تشغيل اللعبة، فإذا وجدت أن نظام التشغيل هو Android ستقوم بتدمير بريمجات استقبال مدخلات الفأرة الموجودة معها على نفس الكائن. أهمية هذه الخطوة تكمن في أن Unity يحاول محاكاة وجود فأرة على شاشات اللمس عن طريق تحويل اللمسات إلى مدخلات فأرة. هذا الأمر يؤدي إلى سلوك غير مرغوب لا يفيد في لعبتنا، لذا نتجنبه عن طريق حذف أي بريمج يقرأ من الفأرة. لنبدأ مع بريمج التحكم بالكاميرا CameraTouchInput والذي سنضيفه على قالبي عنصري الخلفية والأرضية حيث يوجد CameraMouseInput أيضا. هذا البريمج موضح في السرد التالي: using UnityEngine; using System.Collections; public class CameraTouchInput : MonoBehaviour { //سرعة تحريك الكاميرا public float movementSpeed = 0.0075f; //سرعة التقريب والإبعاد public float zoomingSpeed = 0.00075f; //هي يلمس اللاعب الشاشة حاليا بإصبع واحد؟ private bool singleTouch = false; //هل يلمس اللاعب الشاشة حاليا بأصبعين private bool doubleTouch = false; //مرجع لبريمج التحكم بالكاميرا CameraControl camControl; //تستدعى مرة عند بداية التشغيل void Start() { //قم بتدمير بريمج استقبال مدخلات الفأرة إن تم اكتشاف نظام تشغيل الهاتف الذكي if (Application.platform == RuntimePlatform.Android) { CameraMouseInput mouseInput = GetComponent<CameraMouseInput>(); Destroy(mouseInput); } camControl = FindObjectOfType<CameraControl>(); } //يتم استدعاؤها مرة عند تصيير كل إطار في وقت متأخر void LateUpdate() { UpdateSingleTouch(); UpdateDragging(); UpdateDoubleTouch(); UpdateZooming(); } //تتأكد من كون اللاعب يلمس الشاشة بإصبع واحد void UpdateSingleTouch() { if (Input.touchCount == 1) { Touch playerTouch = Input.touches[0]; if (playerTouch.phase == TouchPhase.Began) { //قام اللاعب للتو بوضع إصبعه على الشاشة //هل تم وضع الإصبع على هذا الكائن تحديدا؟ Vector2 touchPos = Camera.main.ScreenToWorldPoint(playerTouch.position); //اجلب مكوّن التصادم الخاص بهذا الكائن Collider2D myCollider = GetComponent<Collider2D>(); //قم بتوليد شعاع قصير جدا ابتداء من موقع اللمس وباتجاه الأعلى واليمين //ومن ثم تحقق من كون هذا الشعاع قد اصطدم بمكوّن التصادم الخاص بهذا الكائن RaycastHit2D hit = Physics2D.Raycast(touchPos, Vector2.one, 0.1f); if (hit.collider == myCollider) { //نعم لقد قام اللاعب بلمس هذا الكائن بإصبع واحد singleTouch = true; } } else if (playerTouch.phase == TouchPhase.Ended || playerTouch.phase == TouchPhase.Canceled) { //قام اللاعب للتو برفع إصبعه عن الشاشة singleTouch = false; } } else { //عدد الأصابع على الشاشة لا يساوي 1 أي أنه //لا يوجد هناك لمسة بإصبع واحد singleTouch = false; } } //تقوم بفحص تحريك اللاعب لإصبع واحد على الشاشة void UpdateDragging() { if (singleTouch) { Touch playerTouch = Input.touches[0]; camControl.Move(playerTouch.deltaPosition * -movementSpeed); } } //تقوم بفحص قيام اللاعب باللمس بأصبعين void UpdateDoubleTouch() { //تأكد من عدم وجود لمسة بإصبع واحد حاليا if (!singleTouch) { if (Input.touchCount == 2) { doubleTouch = true; } else { doubleTouch = false; } } } //تقوم بتحديث التقريب والإبعاد مستخدمة حركة الأصبعين على الشاشة void UpdateZooming() { if (doubleTouch) { //على الشاشة A, Bمواقع الأصبعين المسميين //في كل من الإطار الحالي 2 والإطار السابق 1 Vector2 posA1, posA2, posB1, posB2; Touch a, b; a = Input.touches[0]; b = Input.touches[1]; posA2 = a.position; posB2 = b.position; posA1 = a.position - a.deltaPosition; posB1 = b.position - b.deltaPosition; //تأكد من كون المسافة بين الأصابع قد زادت أو قلت منذ //الإطار السابق float currentDist = Vector2.Distance(posA2, posB2); float prevDist = Vector2.Distance(posA1, posB1); //طرح المسافة السابقة من الحالية سيعطينا //الإشارة الصحيحة للتقريب أو الإبعاد camControl.Zoom((currentDist - prevDist) * zoomingSpeed); } } } آلية عمل هذا البريمج تعتمد على أربعة خطوات رئيسية: التحقق من لمس الشاشة بإصبع واحد.ثم التحقق من تحريك الكاميرا باستخدام الإصبع.ثم التحقق من لمس الشاشة بأصبعين.وأخيرا التأكد من تنفيذ التقريب والإبعاد باستخدام الأصبعين.قراءة مدخلات شاشة اللمس تتم كالآتي: أولا نتعرف على عدد اللمسات على الشاشة عن طريق المتغير Input.touchCount، فإذا كان عدد اللمسات يساوي واحدا، فإن هذا يعني أن اللاعب يضع إصبعا على الشاشة واحتمال أن يقوم بتحريك الكاميرا وارد. نستخدم هنا المتغيرين singleTouch و doubleTouch من أجل تخزين عدد اللمسات التي اكتشفناها. هذه اللمسات تكون مخزنة في المصفوفة Input.touches وهي مصفوفة تحتوي على عناصر من نوع Touch. هذا النوع من المتغيرات يحتوي على معلومات عن كل لمسة على الشاشة. أول هذه المعلومات التي سنتعامل معها هي Touch.phase والتي تمثل المرحلة التي تمر بها اللمسة. فبمجرد أن يضع اللاعب إصبعه على الشاشة ستكون المرحلة هي TouchPhase.Began. اللحظة الأولى للمس الشاشة مهمة جدا، حيث أنها اللحظة التي يجب علينا أن نتحقق من كون اللاعب قد وضع إصبعه على عنصر الخلفية أو الأرضية، وبالتالي نحدد إذا ما كنا سنسمح له بتحريك الكاميرا. فنحن لا نرغب بتحريك الكاميرا إذا كان اللاعب قد وضع إصبعه على المقذوف أو على زر الخروج مثلا. التحقق من كون اللاعب قد وضع يده على عنصر الخلفية أو الأرضية يتم عبر الدّالة ()UpdateSingleTouch، حيث علينا أن نقوم أولا بتحويل موقع الإصبع من إحداثيات الشاشة إلى إحداثيات المشهد تماما كما سبق وفعلنا مع مؤشر الفأرة. بعد ذلك نقوم باستخراج مكوّن التصادم الخاص بهذا العنصر ومن ثم نستخدم بث الأشعة Ray Casting. بث الأشعة طريقة يستخدمها محرك الفيزياء من أجل الكشف عن تقاطع خط مستقيم مع كائن ما في المشهد، وسنستخدمها هنا لنرسم خطا قصيرا جدا من موقع اللمس إلى نقطة شديدة القرب منه، ونرى إن كان هذا الخط يتقاطع مع مكوّن التصادم الذي استخرجناه. يتم تنفيذ بث الأشعة عن طريق الدّالة ()Physics2D.Raycast والتي تأخذ 3 متغيرات: المتغير الأول هو موقع بداية الشعاع، والمتغير الثاني هو الاتجاه الذي سيسير فيه هذا الشعاع، بينما يحدد المتغير الثالث وهو قيمة رقمية أقصى مسافة يمكن أن يقطعها هذا الشعاع. وهنا تلاحظ استخدامنا لمسافة قصيرة جدا، وهي كافية حين يكون موقع البداية موجودا أصلا داخل مكوّن التصادم الذي يتم فحص التصادم معه. القيمة التي ترجعها هذه الدّالة هي من نوع RaycastHit2D، وهي تحتوي على متغير لتخزين مكوّن التصادم الذي اصطدم به الشعاع. بعد ذلك نفحص إذا ما كان المكوّن الذي اصطدم به الشعاع هو نفسه مكوّن التصادم الذي استخرجناه من العنصر الحالي، وإذا تحقق هذا الأمر فإننا نعتمد هذا اللمسة من قبل اللاعب على الخلفية أو الأرضية ونسمح له بالتالي بتحريك الكاميرا عن طريق تغيير قيمة singleTouch إلى true. في حالة كانت اللمسة في مرحلة أخرى مثل TouchPhase.Ended أو TouchPhase.Canceled، فإن هذا يعني أن اللاعب قد رفع إصبعه عن الشاشة، وبالتالي نعيد قيمة singleTouch إلى false. الأمر نفسه سيحدث في حال اكتشفنا أن عدد الأصابع الموجودة على الشاشة Input.touchCount لا يساوي واحدا، بالتالي لن نسمح للاعب في هذه الحالة بتحريك الكاميرا. الدّالة ()UpdateDragging هي المسؤولة عن تحريك الكاميرا، لذا عليها أن تتأكد أولا من قيمة singleTouch وبعد ذلك تقوم بتحريك الكاميرا بالمقدار playerTouch.deltaPosition مضروبا في سرعة الحركة. لاحظ أننا هنا لا نحتاج لتخزين موقع اللمسة السابق حيث أن قيمة الإزاحة تأتينا مباشرة بعكس ما كان عليه الحال حين التعامل مع مؤشر الفأرة. بعد ذلك تقوم الدّالة ()UpdateDoubleTouch بالتحقق من كون اللاعب يضع أصبعين على الشاشة، وفي هذه الحالة تقوم مباشرة بتغيير قيمة doubleTouch إلى true. لاحظ أن اللمس بأصبعين ليس له أي مدلول في اللعبة سوى التقريب والإبعاد بخلاف اللمسة الواحدة التي يمكن أن تستخدم لأكثر من غرض. ولهذا السبب لا نحتاج للتحقق من مواقع الأصبعين بل يكفي وجودهما لمنع أي مدخلات أخرى غير التقريب والإبعاد. أخيرا تقوم الدّالة ()UpdateZooming بالتأكد من وجود أصبعين على الشاشة عبر المتغير doubleTouch وبناء عليها تحسب 4 مواقع وهي كالتالي: posA1: موقع الإصبع الأول خلال الإطار السابق.posA2: موقع الإصبع الأول خلال الإطار الحالي.posB1: موقع الإصبع الثاني خلال الإطار السابق.posB2: موقع الإصبع الثاني خلال الإطار الحالي.لاحظ أننا قمنا بطرح الإزاحة من مواقع الأصابع الحالية حتى نحصل على مواقع الأصابع في الإطار السابق، وذلك لأننا لم نقم بتخزين هذه المواقع من الأساس. بعد ذلك نحسب المسافة بين الأصبعين في الإطار السابق prevDist والإطار الحالي currentDist. تذكر أن الدّالة ()Zoom في البريمج CameraControl تقوم بالتقريب إذا أعطيناها قيمة سالبة وبالإبعاد إذا أعطيناها قيمة موجبة. من أجل ذلك نقوم بطرح المسافة السابقة من المسافة الحالية وضرب الناتج في سرعة التقريب والإبعاد zoomingSpeed. بهذه الطريقة نضمن الحصول على قيمة موجبة إذا حرك اللاعب أصبعيه بعيدا عن بعضهما مما يؤدي للتقريب وقيمة سالبة إذا قرب اللاعب أصبعيه من بعضهما مما يؤدي للإبعاد، وهذا السلوك بطبيعة الحال هو المعتاد عند مستخدمي الهواتف الذكية والأجهزة اللوحية. البريمج الثاني الذي سنتناوله في موضوع استقبال مدخلات شاشة اللمس هو الخاص بإطلاق المقذوفات. تذكر أننا قمنا بإضافة بريمج يسمى LauncherMouseInput على قالب مقلاع إطلاق المقذوفات. البريمج الجديد يسمى LauncherTouchInput وسنقوم بإضافته على نفس القالب من أجل تمكين اللاعب من إطلاق المقذوفات عن طريق اللمس. السرد التالي يوضح هذا البريمج: using UnityEngine; using System.Collections; public class LauncherTouchInput : MonoBehaviour { //مرجع لبريمج إطلاق المقذوفات private Launcher launcher; //تستدعى مرة واحدة عند بداية التشغيل void Start () { //تقوم بتدمير بريمج قراءة مدخلات الفأرة في حال اكتشاف نظام تشغيل الهاتف if (Application.platform == RuntimePlatform.Android) { LauncherMouseInput mouseInput = GetComponent<LauncherMouseInput>(); Destroy(mouseInput); } launcher = GetComponent<Launcher>(); } //تستدعى مرة عند تصيير كل إطار void Update () { UpdateTouchStart(); UpdateDragging(); UpdateRelease(); } //تقوم بالتحقق من كون اللاعب قد وضع اصبعا واحدا //على المقذوف الحالي void UpdateTouchStart() { Projectile currentProj = launcher.currentProjectile; if (currentProj == null) { //لا يوجد ما يمكن فعله return; } if (Input.touchCount == 1) { Touch playerTouch = Input.touches[0]; if (playerTouch.phase == TouchPhase.Began) { //قام اللاعب للتو بلمس الشاشة // تحقق من كون عملية اللمس تمت داخل حدود المقذوف Vector2 touchPos = Camera.main.ScreenToWorldPoint(playerTouch.position); //استخرج مكوّن التصادم الخاص بالمقذوف الحالي Collider2D projectileCollider = currentProj.GetComponent<Collider2D>(); if (projectileCollider.bounds.Contains(touchPos)) { //تمت عملية اللمس داخل حدود المقذوف بالتالي يجب إمساكه launcher.HoldProjectile(); } } } } //تقوم بالتحقق من تحريك اللاعب لإصبعه على الشاشة أثناء إمساك المقذوف void UpdateDragging() { if (Input.touchCount == 1) { Vector2 touchWorldPos = Camera.main.ScreenToWorldPoint(Input.touches[0].position); launcher.DragProjectile(touchWorldPos); } } //تتحقق من رفع اللاعب إصبعه عن الشاشة void UpdateRelease() { if (Input.touchCount == 1) { Touch playerTouch = Input.touches[0]; if (playerTouch.phase == TouchPhase.Ended || playerTouch.phase == TouchPhase.Canceled) { launcher.ReleaseProjectile(); } } } } ينفذ هذا البريمج ثلاث خطوات في كل عملية تحديث: الخطوة الأولى عبر الدّالة ()UpdateTouchStart والتي تتحقق من كون اللاعب قد لمس الشاشة للتو مستخدما إصبعا واحدا. في هذه الحالة تقوم الدّالة بحساب موقع اللمسة في فضاء المشهد ومن ثم فحص ما إذا كان هذا الموقع ضمن حدود مكوّن التصادم الخاص بالمقذوف الحالي المتواجد على مقلاع الإطلاق. إذا كانت هذه اللمسة فعلا داخل حدود المقذوف فهذا يعني أن اللاعب يريد إمساكه، وبالتالي تقوم الدّالة بإمساك المقذوف عبر استدعاء ()HoldProjectile من البريمج Launcher.الخطوة الثانية التي تنفذ عبر ()UpdateDragging تتعلق بوجود إصبع واحد على الشاشة، حيث تقوم بحساب موقع الإصبع في فضاء المشهد وتطلب من البريمج Launcher تحريك المقذوف الحالي إلى هذا الموقع. تذكر أن الأمور الأخرى مثل التأكد من وجود مقذوف أو إذا ما كان ممسوكا أو تم إطلاقه تتم من خلال البريمج Launcher نفسه بالتالي لا نحتاج لفحصها هنا، ولعلك تذكر أننا لم نفحصها أيضا في بريمج قراءة مدخلات الفأرة.أخيرا تقوم الدّالة ()UpdateRelease بالتحقق من كون اللاعب قد رفع إصبعا واحدا عن الشاشة، وذلك من خلال فحص مرحلة اللمس playerTouch.phase إذا ما كانت تساوي Ended أو Canceled وهما الحالتان المتوقعتان حال رفع الإصبع. عند التأكد من هذا الأمر تقوم باستدعاء دالّة إطلاق المقذوف ()ReleaseProjectile.البريمج الأخير الذي يقرأ مدخلات شاشة اللمس هو البريمج الخاص بتنفيذ الهجوم الخاص للمقذوف بعد إطلاقه. هذا الهجوم يتم عبر لمس الشاشة مرة واحدة في أي موقع بعد إطلاق المقذوف. هذا البريمج يسمى SpecialAttackTouchInput ويجب أن تتم إضافته لجميع قوالب المقذوفات التي أنشأناها. هذا البريمج موضح في السرد التالي: using UnityEngine; using System.Collections; public class SpecialAttackTouchInput : MonoBehaviour { //تستدعى مرة واحدة عند بداية التشغيل void Start () { //قم بتدمير بريمج قراءة مدخلات الفأرة في حال اكتشاف نظام تشغيل الهاتف if (Application.platform == RuntimePlatform.Android) { SpecialAttackMouseInput mouseInput = GetComponent<SpecialAttackMouseInput>(); Destroy(mouseInput); } } //تستدعى مرة واحدة عند تصيير كل إطار void Update () { if (Input.touchCount == 1) { Touch playerTouch = Input.touches[0]; if(playerTouch.phase == TouchPhase.Began) { SendMessage("PerformSpecialAttack"); } } } } يمكنك ملاحظة مدى بساطة هذا البريمج، حيث أن كل ما يفعله هو إرسال الرسالة PerformSpecialAttack في حال قام اللاعب بلمس الشاشة مرة واحدة. بهذا تكون اللعبة مكتملة لأجهزة الحاسب والهواتف الذكية، وبقي علينا تصديرها على شكل تطبيق مستقل. تصدير اللعبة بعد إكمالهايوفر محرك Unity إمكانية تصدير الألعاب على مجموعة كبيرة من المنصات، وما يهمنا منها الآن هو منصتا الحاسب الشخصي والهواتف الذكية وتحديدا الأجهزة التي تعمل بنظام Android. تصدير اللعبة يتم عن طريق النافذة التي حددنا منها ترتيب المشاهد وهي: File > Build Settings بالنسبة للتصدير للحواسيب وأجهزة Mac اختر من القائمة اليسرى PC, Max & Linux Standalone لتظهر أمامك الخيارات التالية: أهم هذه الخيارات هو Target Platform والذي تحدد من خلاله نظام التشغيل المستهدف وخيار Architecture والذي يحدد نوع النظام التشغيل سواء كان 32 أو 64 بت. من المهم الانتباه إلى أن الخيار x86_64 يجعل اللعبة تعمل فقط على أنظمة 64 بت حيث أن وجود x86 في الاسم قد يضلل أحيانا فتظن أنه يشمل النظامين. إن كنت تستهدف النظامين 32 و 64 فابق على الخيار الأول x86. بعد ذلك يمكنك الضغط على Build ثم تختار مكان التخزين لتبدأ بعدها عملية التصدير. بالنسبة للتصدير للهواتف الذكية الموضوع أصعب قليلا. سأتحدث هنا بالتفصيل عن طريقة التصدير لمنصة Android فقط لأنني للأسف لا أملك خبرة في التعامل مع أنظمة iOS. قبل التصدير لهذا النظام علينا القيام بتجهيز بعض الأدوات. الخطوة الأولى هو تحميل وتثبيت Android SDK وستحتاج هنا للإصدار الخامس حيث أن الإصدار الخامس من محرك Unity يحتاج لهذه النسخة. يمكنك تحميل المكتبات اللازمة من هذا الرابط https://developer.android.com/sdk/index.html#Other. عند التثبيت يفضل أن تختار موقعا سهلا مثل d:\AndroidSDK. بعد تثبيت Android SDK على جهازك عليك أن تقوم بتنزيل مكتبة واحدة على الأقل عن طريق تشغيل SDK Manager وهو التطبيق المصاحب لـ Android SDK. كل ما تحتاج إليه هنا هو اختيار SDK Platform للإصدار الأخير (حتى كتابة هذه الدروس الإصدار الأخير هو API22 5.1.1). وذلك موضح في الصورة التالة: بعد تنزيل وتثبيت المكتبة علينا أن نخبر Unity عن موقعها وذلك عبر الخيار: Edit > Preferences ومن ثم اختيار External Tools من القائمة اليسرى. بعدها يمكنك تحديد المجلد الذي قمت بتثبيت Android SDK فيه عن طريق الخيار Android SDK Location كما ترى هنا: بما أن اللعبة ستعمل على شاشة الهاتف، يجب علينا أن نجبر النظام على تشغيلها في الوضع الأفقي وليس العمودي. هذا الخيار يمكن ضبطه من خلال القائمة: Edit > Project Settings > Player ومن ثم اختيار رمز نظام Android وفتح مجموعة الإعدادات المسماة Resolution and Presentation. بعدها يجب ضبط Default Orientation على الخيار Landscape Left كما هو موضح في الصورة التالية: الخطوة الأخيرة قبل التصدير هي تحديد معرف الحزمة، وهو عبارة عن اسم على شكل عنوان موقع إنترنت معكوس يبدأ بعنوان موقع الناشر وينتهي باسم المنتج، مثل com.Hsoub.Academy.AngryAnimals. قد تبدو هذه الطريقة في التعريف غريبة بعض الشيء، لكنها موروثة من طريقة تعريف الحزم البرمجية بلغة Java والتي تعتبر اللغة الرئيسية لبرمجة التطبيقات على نظام Android. لا يشترط طبعا أن يكون موقع الإنترنت المستخدم حقيقيا، لكن المهم هو أن تتبع هذا التنسيق في تعريف الحزمة. لإضافة معرّف الحزمة ادخل إلى القائمة: Edit > Project Settings > Player ومن ثم اختر رمز نظام Android من نافذة الخصائص وافتح مجموعة الإعدادات المسماة Other Settings كما هو موضح في الصورة التالية: يمكنك بعدها كتابة معرّف الحزمة في الخانة PlayerSettings.bundleIdentifier كما يمكنك تحديد الإصدار الأدنى الذي يدعمه التطبيق. يستحسن أن تضبط التوافق على الإصدار 4.0 من النظام حيث أنه لا زال شائعا في معظم الأجهزة المتوفرة. بعدها يمكنك أخيرا العودة لشاشة التصدير: File > Build Settings واختيار Android من القائمة اليسرى ومن ثم الضغط على Build واختيار موقع التصدير. سيكون الملف الناتج من نوع APK والذي يمكنك نسخه لجهازك المحمول وتنزيله وتشغيله. قد تضطر لتغيير إعدادات الجهاز حتى يسمح بتنزيل تطبيقات مجهولة المصدر (أي من مصدر غير متجر Google Play) وهذه الإعدادات تختلف من جهاز لآخر. بالنسبة لأجهزة Samsung ستجد هذا الخيار في أعدادات الحماية تحت بند إدارة الجهاز كما هو موضح هنا: تهانينا! لقد قمت ببناء وتصدير لعبة كاملة على هاتفك الذكي أو جهازك اللوحي، ويمكنك الاستمتاع بلعبها ومشاركتها مع أصدقائك.
  4. نواصل في هذا المقال استعراض أساسيات لغة جافا التي يحتاج كل مُطوّر أندرويد الإلمام بها. إذا لم تطّلع على الجزء الأول فأنصحك بقراءته أوّلا قبل مواصلة قراءة هذا المقال. المصفوفات في بعض الأحيان نحتاج إلى تخزين عدة بيانات من نفس النوع والتعامل معها، في هذه الحالة لا يتم استخدام عدة متغيرات للتعامل معها ولكن يتم استخدام المصفوفات Arrays. فالمصفوفات مجموعة متغيرات من نفس النوع وتربطها علاقة ببعضها فيتم تخزينها داخل متغير واحد من النوع مصفوفة، ويتم تعريف المصفوفة بالشكل التالي DataType [] arrayName = new DataType [arraySize]; فمثلا لتعريف مصفوفة من النوع int وتحتوي على 6 عناصر: int [] numArr = new int [6]; هكذا تم حجز 6 أماكن في الذاكرة للمصفوفة numArr، وللوصول لهذه الأماكن للتخزين فيها أو التعامل مع قيمها. numArr[0] = 10; numArr[1] = 5; ... numArr[5] = 3; والعنصر الأول يبدأ من صفر وتنتهي العناصر عند الرقم 5 لتصبح ست عناصر، ويتم التعامل بعد ذلك مع عناصر المصفوفة مثل المتغيرات فمثلًا لجمع رقم ما على إحدى قيمها numArr[3] = numArr[0] + 4; ويتم احتساب خطأ إذا تم الوصول إلى عنصر خارج حدود المصفوفة التي تم تعريفها مثل: numArr[7] = 2 ; //Error وتصنف النوع السابق من المصفوفات على أنه من المصفوفات الساكنة التي عند تحديد عدد عناصرها عند تعريفها فلا يمكن زيادة هذا العدد أو إزالة عناصر من المصفوفة. الدوال الدالة هي مجموعة من الأوامر التي تنفذ فقط عندما نقوم باستدعائها وتستخدم للقيام بالأوامر التي يتكرر استخدامها في مواضع مختلفة في البرنامج بسهولة دون إعادة كتابة الأسطر البرمجية مجددًا مما يجعل البرنامج أكثر وضوحًا. تتميز جافا باحتوائها على مجموعة كبيرة من الدوال الجاهزة التي يمكنك استعمالها مباشرة، كما يمكننا من إنشاء دوال خاصة تؤدي وظائف محددة. تعريف الدوال يتم على النحو التالي AccessModifier ReturnType methodName ( parameters List){ //Do some Actions here } ويحدد AccessModifier طريقة الوصول لهذه الدالة. ونوع البيانات التي سترجعه الدالة يتم تحديده في ReturnType. ثم يتم تحديد الاسم الخاص بالدالة، والـ parameters هي البيانات التي يتم تمريرها للدالة. public int add( int a , int b ) { int sum = a + b; return sum; } في المثال السابق كلمة public تعني أن هذه الدالة يمكن استدعاؤها من أي مكان في البرنامج وهي عكس private والتي تعني أن هذه الدالة لا يمكن استدعاؤها إلا من داخل الصّنف class الذي قام بتعريفها. بعد ذلك تم تحديد النوع الذي ستعيده الدالة عند استدعائها وهو int، والاسم الخاص بهذه الدالة add وتأخذ هذه الدالة قيمتين من النوع int لتعيد ناتج جمعهما. لاستدعاء هذه الدالة من أي مكان داخل البرنامج يتم كتابة اسم الدالة كما بالشكل: int result = add (10 , 15); الأصناف وهو الوحدة الأساسية المبني عليها باقي مفاهيم البرمجة كائنية التّوجّه، وهو عبارة عن وعاء كبير يحتوي على متغيرات ودوال وكائنات. وعند تعريف صنف Class جديد يصبح لديك نوع بيانات جديد يمكنك استخدامه مع باق الأنواع الموجودة بالفعل. لتعريف صنف يتم كتابة كلمة class واختيار اسم له، ثم فتح أقواس تحدد بدايته ونهايته. class ClassName { } تسمى المتغيرات التي يتم تعريفها داخل الصّنف بالخصائص attributes، وتسمى الدوال بالتّوابع methods (يعني سنستعمل "دالة" و "تابع" للدّلالة على نفس المفهوم). وتسمى المتغيرات من الأصناف بالكائنات Objects، فلا يمكن إنشاء كائن دون وجود صنف له. إذا أردنا أن نكتب برنامجًا يعبر عن مكتبة وما تحتويه من كتب فيمكن اعتبار الكتاب على أنه كائن ولإنشاء هذا الكائن ينبغي وجود Class له يحتوي على الخصائص الأساسية لجميع الكتب. class Book{ private String bookName; private String authorName; private int bookCode; public void setBookName(String name){ bookName = name; } public String getBookName(){ return bookName; } public void setAuthorName(String name){ authorName = name; } public String getAuthorName(){ return authorName; } public void setBookCode(int code){ bookCode= code } public int getBookCode(){ return bookCode; } } هكذا قمنا بصنع الصّنف الأساسي لجميع الكتب ويحتوي على خصائص مثل اسم الكتاب واسم الكاتب وهذه البيانات معرّفة private أي كما ذكرنا سابقًا لا يمكن الوصول لها أو تغييرها إلا داخل الصّنف المعرّفة بداخله فقط، وهناك عدة توابع فمنها ما يقوم بتخزين قيم في المتغيرات الخاصة بصنف ومنها ما يعيد القيم المخزنة. لاحظ أننا قمنا بتعريف التوابع كـ public وذلك حتى نستطيع الوصول لها واستدعائها من أي مكان في البرنامج. هناك بعض الدوال التي تم تعريف نوع المُخرجات returnType لها من النوع void وتعني أنها لا تعيد شيئًا. ولإنشاء كائن من هذا الصّنف نكتب: Book b1 = new Book(); Book book2 = new Book(); النصف الأيسر كما اعتدنا سابقًا عند إنشاء متغير جديد، أما النصف الأيمن فيتكون من شقين كلمة new وتعني إنشاء هذا المتغير وحجز مساحة له في الذاكرة واسم الصّنف وبعده أقواس وهو ما يسمى بالـ Constructor. ولأي من هذه الكائنات إذا أردنا وضع قيم للخصائص التي بداخلها يتم على النحو التالي: b1.setBookName(“Learn Java”); book2.setBookName(“Intro to programming Language”); book2.setBookCode(101); String name = b1.getBookName(); وهذه بعض الأمثلة لكيفية استدعاء التّوابع المختلفة من داخل class وباستخدام الكائن، لكل كائن خصائصه الخاصة لا يتشارك فيها مع باق الكائنات التي من نفس Class فلكل منها اسم كتاب يتم تخزين فيه نص معين لا يتشاركان فيه. ولاستدعاء التوابع يتم استخدام (.) بعد اسم الكائن ثم كتابة اسم التابع وتمرير المتغيرات التي تتعامل معها إن وجدت حسب تعريفنا للتّابع داخل الصّنف. لاحظ أن عند إنشاء صنف جديد نبدأ اسمه بحرف كبير دائمًا، ويمكنك إنشاء أي عدد من الكائنات من الصّنف كما ذكرنا في المثال السابق. تابع البناء Constructor تتواجد داخل كل صنف تابع خاص يُدعى Constructor يتم استدعاؤه أثناء إنشاء كائن جديد فهو تابع مهم جدًا لإنشاء الكائنات ويقوم المترجم بإنشاء هذا تابع بناء فارغ بشكل افتراضي إذا لم يتم تعريفها من قبل المطوّر. وتكتب كالتالي: public ClassName(parameter List){ //Do some Actions here } يجب أن يكون اسم تابع البناءconstructor نفس اسم الصنف ومن النوع public ويمكنك تعريف أكثر من constructor داخل نفس الصّنف ولا يوجد نوع إرجاع returnType لـلـ constructor. يمكننا من خلال constructor إدخال قيم مباشرة في الخصائص الموجودة في الكائن بدلًا من استدعاء دالة لكل خاصية. class Book{ private String bookName; private String authorName; private int bookCode; public Book(String name,String author,int code){ bookName = name; authorName = author; bookCode = code; } public String getBookName(){ return bookName; } public String getAuthorName(){ return authorName; } public int getBookCode(){ return bookCode; } } في كل مرة يتم إنشاء كائن جديد، يجب استدعاء تابع البناء constructor حتى يتم إنشاء هذا الكائن. لإنشاء كائنات بعد هذا التعديل: Book b1 = new Book (“Learn Java”,”M.K Dave”, 150); هنا تم تعريف كافة الخصائص لحظة إنشاء الكائن، ويتم التعامل مع هذا الكائن بشكل طبيعي كما تعاملنا معه مسبقًا. الوراثة يعتبر مفهوم الوراثة من أهم المفاهيم في البرمجة كائنية التّوجّه ويعني يمكننا إنشاء أصناف ترث من صنف آخر الخصائص والتّوابع المتواجدة به دون تعريفها من جديد، مما يسمح بالتركيز على الخصائص والتّوابع التي يتميز بها الصّنف الجديد. وتستخدم الكلمة extends لتطبيق مفهوم الوراثة: class Shape{ protected int width; protected int height; public void setWidth(int a){ width = a; } public void setHeight(int b){ height = b; } } إذا كان لدينا هذا الصّنف وهو يُمثل شكلًا عامًا له طول وعرض وأردنا أن ننشئ شكلًا آخر (مربع) يرث من هذا الصّنف ويضيف خصائص أكثر: class Square extends Shape{ public int getArea(){ return width * height; } } وبذلك أصبح Square يحتوي على الدالتين setWidth و setHeight بالإضافة للدالة الجديدة التي قام بتعريفها getArea، وبذلك استطعنا أن نعطي المزيد من الخصائص للشكل الجديد. وإذا أردنا أن نصنع شكلًا آخر (مثلث): class Triangle extends Shape{ public int getArea(){ return 0.5*width * height; } } لا يمكن للصّنف أن يرث من أكثر من أب. وبتطبيق هذا المفهوم على الدرس السابق نجد أن أندرويد يحتوي على صنف اسمه View وهو الأب لكل العناصر الخاصة بواجهة المستخدم ويحدد الخصائص الأساسية والمشتركة بينهم جميعًا ويرث منه العناصر الخاصة مثل TextView وهو View ولكن يعرض نصًا فقط أو Button وأيًضا View قابل للضغط ويقوم بمهمة محددة عند الضغط عليه. وإذا قمنا بالتعديل على المثال السابق واستخدمنا في Shape تابع البناء لتعريف الطول والعرض: class Shape{ protected int width; protected int height; public Shape(int a ,int b){ width = a; height = b; } } ينبغي علينا أن نعرف دالة بناء للصّنف التي ترث منه: class Square extends Shape{ public Shape(int w ,int h){ super(w,h); } public int getArea(){ return width * height; } } وتستخدم super لاستدعاء Constructor الخاص بالأب وتمرير له القيم التي يحتاجها، وذلك لأن عند إنشاء كائن من Square والذي يرث من Shape: Sqaure s = new Square(10,10); يتم بناء الأب أولًا Shape ثم الابن Square حتى يكتمل بناء الكائن s بشكل صحيح. لذا يتم تمرير في دالة البناء الخاصة بالابن ما تحتاجه دالة البناء الخاصة بالأب، ولم نقوم بذلك قبل التعديل لأننا كنا نستعمل دالة بناء فارغة في الأب. التحويل من نوع بيانات إلى آخر تمكننا لغات البرمجة من التحويل من نوع بيانات إلى آخر باستخدام مفهوم Casting، فمثلًا إذا كان لدينا متغير من النوع double: double d = 12.5478; ونريد تحويله إلى رقم صحيح: int x = (int) d; نفعل ذلك عن طريق كتابة نوع المتغير بين القوسين وبعدها اسم المتغير الذي نريد تحويله كما في المثال. وهناك بعض الشروط على التحويل بين الأنواع بهذه الطريقة فلا يمكن تحويل String إلى int مثلًا لاختلاف النوعين عن بعضهما. ويمكن تحويل من صنف إلى آخر شريطة أن يكون بينهما علاقة الوراثة. Shape sh = new Shape(15,15); Square s = (Square) sh; بهذا نكون قد وصلنا إلى نهاية هذا الدّرس، في انتظار تجربتكم وآرائكم. إن كانت لديك أيّة أسئلة فلا تتردّد في طرحها.
  5. بعد أن أنشأنا أول مشروع في Android Studio وقمنا بتجربة التطبيق على المحاكي سنتعلم الآن أساسيات التعامل مع واجهة المستخدم وكيفية إنشاء العناصر المختلفة بداخلها. طرق إنشاء واجهة المستخدم لتطبيق أندرويد في نظام تشغيل أندرويد هناك طريقتان لإنشاء واجهة المستخدم الخاصة بالتطبيق: التعريف في الملف الخاص بها layout باستخدام لغة XML: وتتميز هذه الطريقة بأنها تقوم بفصل عناصر الواجهة عن الشيفرات البرمجية بلغة الجافا والتي تحدد وظيفة التطبيق الأصلية مما يُسهل عملية تطوير التطبيقات واكتشاف الأخطاء وتصحيحها وتجعل التطبيق أكثر مرونة لدعم أحجام الشاشات المختلفة واللغات المختلفة. تعريف عناصر واجهة المستخدم باستخدام الشيفرات البرمجية بلغة الجافا حيث تتكون الواجهة وقت تشغيل التطبيق وعمله لدى المستخدم. كما يمكنك استخدام هاتين الطريقتين معًا عن طريق تعريف العناصر في ملف layout وباستخدام الشيفرة البرمجية تقوم بتغيير خصائصها بناءً على تفاعل المستخدم مع هذه العناصر، وتلك هي الطريقة التي سنقوم باستخدامها. عناصر واجهة المستخدم تنقسم عناصر واجهة المستخدم إلى نوعين View و ViewGroup: View هو عنصر يقوم برسم شيء ما على الشاشة حيث يستطيع المستخدم التفاعل مع هذا الشيء. ViewGroup هو حاوية غير مرئية تستطيع أن تحمل بداخلها العناصر المكوَنة للواجهة. ويتميز كل نوع من ViewGroup بطريقة فريدة ومختلفة لعرض العناصر الأخرى بداخله وتُسمى العناصر المتواجدة داخل ViewGroup بالأبناء. يمكن أن يحتوي عنصر ViewGroup على عناصر من النوع ViewGroup أيضًا أو من النوع View وذلك للوصول إلى التصميم المنشود لواجهة المستخدم كما توضح الصورة التالية: يوفر أندرويد للمطورين مجموعة من Views و ViewGroup الجاهزة والتي تقوم بالوظائف الشائعة في التطبيقات. أمثلة على ViewGroup Linear Layout: ويتم ترتيب العناصر بداخله في اتجاه واحد إما بشكل أفقي أو رأسي. Relative Layout: ويتم ترتيب العناصر بداخله نسبة إلى عنصر أخر ويتم استخدامه عند صنع واجهة أكثر تعقيدا يصعُب معها استخدام Linear Layout. أمثلة على View Button TextView ImageView أساسيات تصميم الواجهات باستخدام XML توفر لغة XML طريقة سهلة وسريعة لصنع واجهة المستخدم وتتشابه مع طريقة صناعة صفحات الويب باستخدام HTML. كما ذكرنا سابقًا تتواجد الملفات الخاصة بالواجهة في مجلد res/layout بامتداد XML. يجب أن تحتوي ملفات XML على عنصر يسمى root ويعتبر هذا العنصر الحاوية الرئيسية لباقي العناصر ويكون هذا العنصر إما ViewGroup أوView، كما يجب أن يحتوي على XML namespace وهو من المعايير القياسية الخاصة بـ XML. تعريف الـ namespace بسيط وثابت لكل الـ root elements ويكون على النحو التالي xmlns:android="http://schemas.android.com/apk/res/android" وبعد تعريف الـ root element يمكنك بعدها إضافة عناصر أخرى لاستكمال واجهة المستخدم. وفي أغلب التطبيقات يكون الـ root من النوع ViewGroup لقدرته على إضافة أبناء له، على عكس View والذي لا يمكن أن يحتوي على أبناء. يتكون كل عنصر من عناصر XML من وسم الفتح <ElementName>، ووسم الغلق <ElementName/>. وبين وسم الفتح والغلق يتم وضع العناصر الأخرى والتي تعتبر أبناء ViewGroup، وبما أنه لا يوجد للعنصر View لأبناء فيمكن الاستغناء فيه عن وسم الغلق واستخدام وسم الغلق الذاتي </ElementName>. لتوضيح الأمر أكثر سنقوم بصنع واجهة مستخدم مشابهة لتطبيق أهلًا بالعالم Hello World. قم بفتح الملف content_main.xml المتواجد بداخل المجلد res/layout، ثم قم بمسح كافة الشيفرة المكتوبة. بعد عنصر root الخاص بالواجهة أضف عنصر ViewGroup من نوع LinearLayout كالتالي: <LinearLayout> قم بتحديد XML namespace الخاص بالعنصر LinearLayout: <LinearLayout xmlns:android=http://schemas.android.com/apk/res/android > لكل عنصر من عناصر واجهة المستخدم سواء كان View أو ViewGroup العديد من الخصائص والتي يمكن التحكم بها وتخصيصها عن طريق XML أو عن طريق شيفرة الجافا وقد تكون هذه الخصائص خاصة بهذا العنصر فقط أو خصائص مشتركة لدى أكثر من عنصر. ومن الخصائص الهامة تحديد العرض والارتفاع الخاصين بالعنصر، ولتحديد العرض للعنصر نستخدم الخاصية layout_width: android:layout_width="Value" ونستبدل Value بقيمة العرض، وهناك عدة طرق لتحديد قيمة العرض، فيمكن تحديدها عن طريق قيم معرَفة مسبقًا للنظام مثل "match_parent" وتعني أن يكون عرض العنصر نفس عرض العنصر الأب، وإذا كان العنصر الأب هو root فالأب هنا المقصود به شاشة الهاتف. وباستخدام نفس الطريقة يتم تحديد الارتفاع الخاص بالعنصر عن طريق خاصية layout_height: android:layout_height ="Value" ليصبح الشكل النهائي للعنصر: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> </LinearLayout> لاحظ أنه يتم تحديد كافة الخصائص الخاصة بالعنصر داخل وسم الفتح ويتم أيضًا وضع XML namespace معهم. سنقوم بإضافة عنصر أخر للواجهة من نوع View وهو TextView والذي يستطيع أن يحمل بداخله نصًا ويمكن للمستخدم قراءته ولا يمكنه تغييره: <TextView /> سنقوم بتحديد الخصائص الخاصة بهذا العنصر وكما ذكرنا أن خاصتيَ العرض والارتفاع من الخصائص الهامة التي ينبغي تحديدها لكافة العناصر. <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" /> كان من الممكن استخدام قيمة "match_parent" ولكننا قمنا باستخدام "wrap_content" والتي تعني أن يكون العرض أو الارتفاع يشغل القدر المطلوب للنص فقط، أما إذا استخدمنا "match_parent" فسيشغل العنصر كل المساحة الخاصة بالأب والذي هو LinearLayout والذي يشغل مساحة الشاشة كلها كما حددنا ذلك سابقًا. لتحديد النص المطلوب كتابته داخل العنصر، نقوم بإضافة السطر التالي: android:text="Hello World!" هذا هو الشكل الكلي لملف XML: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" /> </LinearLayout> الآن قم بتشغيل التطبيق على المحاكي وستجده يعمل بالشكل المطلوب. ملاحظات العنصر TextView متواجد بين وسميَ الفتح والغلق الخاصين بـ LinearLayout ويُعتبر TextView ابنًا لـ LinearLayout. يستخدم العنصر TextView وسم الغلق الذاتي. يتم تحديد الخصائص للعنصر داخل وسم الفتح الخاص به فقط. التطبيق الثاني سنقوم بتطوير المثال السابق واستخدام عناصر أخرى في التطبيق لصنع واجهة مثل الصورة التالية: الاختلاف هنا في إضافة عنصر جديد للواجهة وهو Button. لإضافة هذا العنصر يتم استخدام العنصر </ Button> وإضافته داخل LinearLayout. نقوم بتحديد الطول، الارتفاع والنص الخاص بالزر كالتالي: <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Click me"/> سيظهر الزر بجانب النص السابق: ولتغيير موضع الزر يجب تغيير اتجاه العناصر داخل عنصر LinearLayout، ولتغيير الاتجاه للرأسي بدلًا من الأفقي نستخدم الخاصية orientation: android:orientation="vertical" حيث أن الاتجاه يكون أفقيًا بشكل افتراضي. ليصبح ملف XML على النحو التالي: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Click me"/> </LinearLayout> ملاحظات أي عنصر من النوع View يستخدم وسم الغلق الذاتي لأنه لا يستطيع أن يحتوي على أبناء أو عناصر أخرى بداخله. إذا قمت بكتابة العنصر Button أولًا بداخل LinearLayout ثم العنصر TextView سيتم تغيير الترتيب في الواجهة أيضًا. التطبيق الثالث يحتوي هذا التطبيق على نص "Android Image" وصورة للأندرويد وزر يتم ترتيبها بشكل رأسي. لصنع واجهة المستخدم سنختار LinearLayout كعنصر root وتغيير الاتجاه الخاص به للرأسي. نقوم بعدها بإضافة عنصر TextView وكتابة النص الخاص به، ولكن يمكن ملاحظة أن حجم الخط الخاص بالنص أكبر من الأمثلة السابقة لذا يجب تغيير حجم الخط عن طريق الخاصية textSize: android:textSize="40sp" ويتم تحديد الخط بوحدة "sp" وهي وحدة خاصة في نظام أندرويد لتحديد حجم الخط دون الاعتماد على كثافة البكسل المكوَنة للشاشة ليظهر النص دائمًا بنفس الحجم باختلاف أحجام شاشات الهواتف. لتغيير موضع النص ليكون في منتصف الواجهة نقوم باستخدام خاصية الجاذبية layout_gravity: android:layout_gravity="center" نقوم بإضافة العنصر ImageView لعرض الصور وتحديد الطول والارتفاع كالسابق، ولتحديد الصورة المعروضة بداخله نقوم بوضع الصورة في الملف res/drawable وعرضها داخل العنصر باستخدام الخاصية src: android:src="@drawable/andimg" تستخدم @ للإشارة إلى عنصر داخل الملف وبعدها يتم كتابة اسم الملف وهو drawable ثم تحديد اسم الصورة المتواجدة بداخله. نضيف بعدها العنصر Button ونقوم بتحديد النص الخاص به مع ملاحظة أن العرض الخاص به يستغل كافة مساحة الشاشة وليس على قدر المحتوى فقط. ليصبح ملف XML على النحو التالي: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Android Image" android:textSize="40sp" android:layout_gravity="center" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/andimg"/> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Say Hi"/> </LinearLayout> التطبيق الرابع نريد في هذا التطبيق صُنع واجهة مركبة كما في الصورة التالية: سيكون الـ root مثل الأمثلة السابقة من النوع LinearLayout ولكن هل اتجاه العناصر بداخله أفقي أم رأسي؟ ستجد العناصر متواجدة في الاتجاهين. لصنع هذه الواجهة سنستخدم أكثر من ViewGroup من النوع LinearLayout ونغير اتجاهاتها لصنع الشكل المطلوب كما في الصورة التالية: سيكون العنصر root من النوع LinearLayout والاتجاه بداخله رأسي، ويحتوي على LinearLayout آخر الاتجاه بداخله أفقي ونضع بداخله العنصرين المتجاورين كما في الصورة السابقة. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> </LinearLayout> </LinearLayout> نضع العنصرين ImageView و EditText داخل العنصر LinearLayout الثاني. نقوم بتحديد العرض والارتفاع واختيار الصورة من ملف drawable للعنصر ImageView مثل ما فعلنا في التطبيق السابق، وبالنسبة للعنصر الآخر EditText والذي يٌستخدم لإدخال نص من المستخدم، نقوم بتحديد عرضه وارتفاعه، بالإضافة إلى عرض نص يعبر عن المحتوى الذي يجب على المستخدم إدخاله باستخدام الخاصية hint: android:hint="Write your name here" نُضيف العنصر الأخير من النوع Button داخل LinearLayout الأول ولوضعه جهة اليمين نستخدم الخاصية layout_gravity: android:layout_gravity="right" ليُصبح ملف XML على الشكل التالي: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher"/> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Write your name here"/> </LinearLayout> <Button android:layout_width="wrap_content" android:layout_height="70dp" android:text="Done" android:layout_gravity="right"/> </LinearLayout> ملاحظات تم تحديد الارتفاع الخاص بـ LinearLayout الثاني بـ "wrap_content" حتى يشغل الارتفاع الخاص بالعناصر فقط ولا يشغل مساحة الشاشة كلها. تم استخدام طريقة مختلفة لتحديد الارتفاع الخاص بـالعنصر Button، وتستخدم هذه الطريقة في حالة لم نرد استخدام القيم المعرَفة داخل النظام مثل "match_parent" أو "wrap_content" واستخدام قيم مختلفة. وتُستخدم الوحدة "dp" كوحدة لتحديد الأبعاد دون الاعتماد على كثافة البكسل المكوَنة للشاشة ليظهر العنصر بنفس الأبعاد دائمًا باختلاف أحجام شاشات الهواتف. التطبيق الخامس هناك طريقة أخرى لإنشاء واجهة المستخدم كما في التطبيق السابق، حيث نقوم باختيار root من النوع RelativeLayout للوصول لنفس الشكل أيضًا كما في الصورة السابقة. في RelativeLayout يتم وضع العناصر نسبة إلى عناصر أخرى متواجدة لذلك ينبغي تحديد لكل عنصر ID مميز له لأن عند وضع عنصرين من نفس النوع (Button مثلا) سيكون اسم كل منهما Button ولا نستطيع عندها التفريق بين الزر الأول أو الثاني وللتفرقة بينهم يتم استخدام خاصية id: android:id="@+id/btn1" ويتم كتابة ID الخاص بالعنصر بعد كلمة /id+@ ويكون هذا ID مميز لهذا العنصر فقط ولا يتشابه معه عنصر آخر. وهناك استخدام آخر لـ ID عند التحكم في العناصر باستخدام شيفرة الجافا. وبهذا يصبح ملف XML على الشكل التالي: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" android:id="@+id/img" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Write your name here" android:id="@+id/edttxt" android:layout_toRightOf="@+id/img" /> <Button android:layout_width="wrap_content" android:layout_height="70dp" android:text="Done" android:id="@+id/btn1" android:layout_below="@+id/edttxt" android:layout_alignParentRight="true"/> </RelativeLayout> عند تشغيل التطبيق على المحاكي سيقوم برسم نفس الشكل السابق. ملاحظات تم تحديد ID خاص لكل العناصر. هناك بعض الخصائص التي تحدد موضع كل عنصر بالنسبة لعنصر آخر. مثل الخاصية layout_toRightOf المستخدمة للعنصر EditText وذلك لوضعه بجانب العنصر ImageView جهة اليمين: android:layout_toRightOf="@+id/img" والخاصيتين layout_below و layout_alignParentRight المستخدمتين مع العنصر Button وذلك لوضعه أسفل العنصر EditText و بمحاذاة الشاشة من جهة اليمين: android:layout_below="@+id/edttxt" android:layout_alignParentRight="true" بهذا نكون قد وصلنا إلى نهاية ثاني دروسنا من سلسلة أندرويد للمبتدئين، في انتظار تجربتكم وآرائكم.
  6. تُعتبر منصّة Xamarin في الوقت الحالي، من أهمّ منصّات تطوير تطبيقات الأجهزة المحمولة المتوفّرة، والتي يبدو أنّها ستبقى على الواجهة لوقت ليس بالقليل، خصوصًا بعد استحواذ شركة مايكروسوفت على الشركة المنتجة لها والتي تحمل أيضًا نفس الاسم (Xamarin). قد تكون متحفّزًا للدخول في عالم تطوير تطبيقات الأجهزة المحمولة (كما كنت عندما تعرّفت للمرّة الأولى على Xamarin) ومستعدًّا لكتابة الشيفرة مباشرةً، ولكن لنصبر قليلًا، ولنفهم بدايات الأمور، ومبدأ عمل Xamarin بشكل أفضل ضمن هذا المقال، الذي يُعدّ الأوّل في سلسلة مقالات سترشدك إلى كيفيّة تطوير تطبيقات باستخدام منصّة Xamarin مع إنشاء تطبيقات عمليّة من خلالها. مقدمة تمّ تأسيس شركة Xamarin (وتُقرأ زامارين) في عام 2011، من قِبَل نفس المهندسين الذين صمّموا مشروع Mono. ومشروع Mono وإخوانه مثل Mono for android و Mono Touch، هي عبارة عن منصّات لتشغيل تطبيقات مكتوبة بلغة سي شارب C# على أنظمة التشغيل: Linux و Android و iOS على الترتيب. يمكن باستخدام Xamarin إنشاء تطبيقات أصليّة Native Apps لأجهزة Android و iOS و Mac وويندوز بلغة برمجة ليست معتمدة رسميًا بالنسبة إليها. سنركّز في هذه السلسلة على بناء تطبيقات تعمل على نظام التشغيل Android. لمن هذه السلسلة؟ إذا كان لديك إلمام جيّد بلغة سي شارب ومكتبة الأصناف الأساسيّة BCL ضمن إطار العمل دوت نت بالإضافة إلى معرفة أوليّة بطريقة استخدام بيئة التطوير Visual Studio 2015، وتمتلك الشغف والصبر لتعلّم كيفيّة برمجة تطبيقات للأجهزة المحمولة باستخدام Xamarin فإنّ هذه السلسلة هي بالتأكيد لك. لدينا هنا في أكاديميّة حسوب سلسلة تعليميّة خاصّة بلغة سي شارب يمكن الاستفادة منها. الحاجة إلى Xamarin برزت الحاجة إلى Xamarin بسبب طبيعة تطبيقات الأجهزة المحمولة وعملها على أنظمة تشغيل مختلفة تعود إلى شركات متنافسة. توجد في الوقت الحالي ثلاثة أنظمة تشغيل مسيطرة على السوق بصورة متفاوتة وهي: iOS لشركة Apple وAndroid لشركة Google وWindows Phone لشركة Microsoft. تختلف هذه الأنظمة في العديد من النواحي، التي سنناقشها فيما يلي. تجربة المستخدم بالنسبة إلى تجربة المستخدم يوجد تشابه في هذه الأنظمة من ناحية تقديم الواجهات الرسوميّة للمستخدم والتفاعل مع الجهاز من خلال اللمس أو اللمس المتعدّد، ولكنّ هناك اختلافات في التفاصيل. فلكلّ نظام تشغيل وسائل مختلفة في التنقّل بين صفحات التطبيق، وفي تقديم البيانات، وفي العمل مع القوائم، وغيرها من التفاصيل الأخرى التي تتطلّب أن يسلك المطوّر developer منحىً مختلفًا من أجل كلّ نظام تشغيل. بيئات تطوير ولغات برمجة مختلفة أمّا بالنسبة للغات البرمجة وبيئات التطوير، فهذا أمر آخر. فلكلّ نظام تشغيل متطلّباته الخاصّة التي ألخّصها بشكل سريع فيما يلي: لإنشاء تطبيقات على أنظمة iOS فأنت تحتاج إلى إتقان لغة البرمجة Objective-C أو لغة البرمجة Swift. وأن تمتلك حاسوب MacBook (أي إصدار) مع بيئة التطوير Xcode. لإنشاء تطبيقات على أنظمة Android فستحتاج إلى لغة جافا Java مع بيئة التطوير Android Studio التي تعمل على العديد من أنظمة التشغيل. أمّا لإنشاء تطبيقات تعمل على Windows Phone أو Windows 10 Mobile فأنت بحاجة إلى لغة البرمجة سي شارب C# مع حاسوب يشغّل ويندوز، وإلى بيئة التطوير Visual Studio. واجهات برمجية مختلفة تعتمد جميع أنظمة التشغيل السابقة على واجهات تطبيق برمجيّة API مختلفة. مع أنّه يوجد بعض الشبه بالنسبة للكائنات المتعلّقة بواجهة المستخدم user-interface. فعلى سبيل المثال، جميع الأنظمة السابقة توفّر وسيلة للمستخدم لاختيار حالة منطقيّة Boolean والتي يمكن تمثيلها بـ True أو False ففي iOS يصنّف هذا الكائن على أنّه view اسمه UISwitch، أمّا في أندرويد فهو widget اسمها Switch، وفي ويندوز فهو control ويسمّى ToggleSwitch. الحل الذي توفره Xamarin يمكن تجاوز جميع النقاط السابقة من خلال Xamarin لأنّها توفّر لغة برمجة وحيدة وهي سي شارب يمكن استخدامها لكتابة تطبيقات على أيّ نظام تشغيل. كما أنّها توفّر بيئة تطوير متقدّمة ووحيدة وهي Visual Studio 2015 لكتابة هذه التطبيقات (يمكن استخدام بيئة التطوير Xamarin Studio أيضًا بالنسبة لنظام Mac). بالإضافة إلى أنّها وحّدت الواجهات البرمجيّة المختلفة API ضمن واجهة برمجيّة وحيدة يتعامل معها المطوّر. لغة سي شارب غنيّة عن التعريف. فهي لغة قويّة ومرنة وغنيّة جدًا ودائمة التطوير. لقد أصبحت بحق من أكثر لغات البرمجة تطوّرًا وحداثةً. ولسنا هنا بصدد تفضيل لغة برمجة على أخرى، ولكن بحسب خبرتي الشخصيّة، واطّلاعي على العديد من لغات البرمجة الأخرى، تستطيع القول بأنّ سي شارب تحتلّ موقعًا مرموقًا بينها. يستطيع المطوّرون من أجل تطبيق محدّد، كتابة شيفرة واحدة مشتركة لجميع أنظمة التشغيل السابقة بدون أيّة تعديلات تُذكر عليها فيما يتعلّق بمنطق العمل business logic داخل التطبيق وبما يتضمنّه من عمليّات برمجيّة لا تتعلّق بنوع الجهاز المحمول أو نظام التشغيل الذي يعمل عليه. نسمّي هذه الشيفرة بالشيفرة المستقلة عن نظام التشغيل platform independent. أمّا إذا تطلّب الأمر من التطبيق أن يتعامل مع العتاد الصلب للجهاز الذي يعمل عليه (مثل الكاميرا أو حسّاس GPS مثلًا)، فيمكن عندها كتابة أجزاء من الشيفرة التي تراعي خصوصيّة كل نظام تشغيل، نسمّي مثل هذه الشيفرة بالشيفرة المرتبطة بنظام التشغيل platform dependent. المكونات الرئيسية لمنصة Xamarin ركّزت شركة Xamarin منذ نشأتها على التكنولوجيا الخاصّة بالمترجمات Compilers. وقد أصدرت الشركة ثلاث مجموعات أساسيّة من مكتبات دوت نت .NET وهي: Xamarin.Mac و Xamarin.iOS و Xamarin.Andorid التي تشكّل بمجموعها منصّة Xamarin. تسمح هذه المكتبات للمطوّرين بكتابة تطبيقات أصليّة native apps لكلّ من أنظمة التشغيل الموافقة. ذكرنا قبل قليل أنّ هناك جزء من شيفرة التطبيق البرمجيّة يتكرّر عادةً من أجل كل نظام تشغيل، يمكن عزل هذا الجزء ووضعه ضمن مشروع منفصل في برنامج Visual Studio. لهذا المشروع نوعين: مشروع الأصول المشتركة Shared Asset Project أو اختصارًا SAP. وهو عبارة عن مجموعة من ملفات الشيفرة بالإضافة إلى ملفّات الأصول assets مثل الصور وغيرها الموجودة ضمن المشروع والتي يمكن مشاركتها مع باقي المشاريع الأخرى ضمن نفس الحل Solution في Visual Studio. مكتبة الأصناف المحمولة Portable Class Library أو اختصارًا PCL. وهي تضمّ الشيفرة التي نرغب بمشاركتها وذلك ضمن مكتبة ربط ديناميكي dynamic link library أو اختصارًا DLL. سنركّز في هذه السلسلة على هذا النوع. أمّا بالنسبة للشيفرة التي تتعلّق بنظام التشغيل، فتوضع ضمن مشروع منفصل ضمن Visual Studio لكلّ نظام تشغيل نرغب بأن ندعمه. ففي الحالة العامّة، يمكن أن يكون لدينا ثلاثة مشاريع منفصلة لكل من Android و iOS و Windows Phone. تستفيد جميعها من الشيفرة المشتركة والمستقلّة عن نظام التشغيل الموجودة في PCL أو SAP. وبالمجمل تكون جميع تلك المشاريع ضمن نفس الحل Solution ضمن Visual Studio. انظر إلى الشكل التالي الذي يوضّح كيفيّة تفاعل التطبيق المكتوب لأنظمة التشغيل المختلفة مع مكوّنات Xamarin: يمثّل الشكل السابق تطبيقًا واحدًا. نلاحظ في السطر الثاني الأشكال المختلفة لهذا التطبيق على أنظمة التشغيل المختلفة. انظر كيف تتفاعل هذه الأشكال المختلفة مع مشروع PCL أو SAP للاستفادة من الشيفرة المشتركة التي لا تتعلّق بنظام تشغيل محدّد. انظر إلى تطبيق iOS مثلًا كيف يتفاعل أيضًا مع المكتبة Xamarin.iOS التي توفّر وسيلة للربط bindings بينه وبين واجهة التطبيقات البرمجيّة API لنظام التشغيل iOS. ينطبق نفس الأمر تماماً بالنسبة لتطبيق أندرويد. الاستثناء الوحيد هو بالنسبة لتطبيق ويندوز الذي لا يحتاج إلى وسيط (وسيلة للربط) في هذه الحالة، حيث يمكنه الاتصال مباشرةً مع واجهة API لنظام التشغيل ويندوز (بصورة أدق Windows Phone). نماذج Xamarin ابتكرت شركة Xamarin في عام 2014 ما يُعرف بنماذج Xamarin أو Xamarin Forms. تسمح هذه المنصّة للمطوّرين بأن يكتبوا شيفرة برمجيّة لواجهة المستخدم user interface بحيث يمكن تحويل هذه الشيفرة مباشرةً إلى تطبيقات تعمل على أجهزة أندرويد و iOS وويندوز. في الحقيقة أصبحت Xamarin Forms تدعم إنشاء تطبيقات عامّة Universal Windows Platform على الأجهزة التي تشغّل نسخ ويندوز المختلفة مثل Windows Phone و Windows 10 و Windows 10 Mobile و Windows 8.1 و Windows 8.1 Phone. بالنسبة لبنية الحل Solution في Visual Studio فلن يتغيّر كثيرًا، باستثناء أنّ المشاريع المنفصلة الخاصّة بأنظمة التشغيل السابقة ستكون صغيرة على نحو ملحوظ بسبب وجود كميّة قليلة من الشيفرة البرمجيّة ضمنها. سيتضمّن مشروع PCL أو SAP في هذه الحالة الشيفرة المشتركة والمستقلّة عن أنظمة التشغيل كما اتفقنا على ذلك من قبل، بالإضافة إلى الشيفرة البرمجيّة المسؤولة عن الإظهار والتعامل مع واجهة المستخدم. أي أنّ منصّة Xamarin Forms تسمح لنا بكتابة شيفرة برمجيّة واحدة تعمل مباشرةً على أنظمة التشغيل المختلفة. انظر الشكل التالي لفهم آليّة عمل التطبيقات التي تعتمد Xamarin Forms. تعتمد التطبيقات المكتوبة لأنظمة التشغيل المختلفة في هذه الحالة على مشروع PCL أو SAP بشكل كليّ في التواصل مع الواجهات البرمجيّة API. وهكذا من الممكن في الكثير من التطبيقات كتابة شيفرة واحدة فقط تعمل على جميع الأجهزة! باستثناء الحالات التي يكون من الضروري فيها كتابة شيفرة مخصّصة لنظام تشغيل محدّد. في المستقبل قد يتغيّر هذا الأمر، فقد يكون من الممكن كتابة شيفرة واحدة فقط تعمل على جميع الأجهزة مهما كان نوع هذه الشيفرة. سنتحدّث في هذه السلسلة عن Xamarin Forms بشكل أساسيّ. كيفية عمل Xamarin بالنسبة لتطبيقات iOS فيعمل مترجم سي شارب الخاص بـ Xamarin على ترجمة الشيفرة البرمجيّة إلى لغة مايكروسوفت الوسيطيّة MSIL، ثمّ يُستخدم مترجم Apple على نظام تشغيل Mac لتوليد رُماز أصليّ native code يعمل على iOS كما لو أنّه تطبيق مكتوب بلغة Objective-C تمامًا. أمّا بالنسبة لتطبيقات Android، فسيولّد المترجم أيضًا لغة MSIL التي ستعمل في هذه الحالة على بيئة تنفيذ مشتركة CLR مخصّصة للعمل على أندرويد. وستكون التطبيقات الناتجة في هذه الحالة تشبه أيضًا إلى حدّ كبير تلك المنشأة باستخدام لغة جافا وبيئة التطوير Android Studio الخاصّة بأندرويد. وأخيرًا بالنسبة للتطبيقات التي تعمل على Windows Phone و Windows 10 Mobile، فالتطبيقات مدعومة بشكل واضح، وستعمل كالتطبيقات الأصليّة المنشأة باستخدام Visual Studio بدون استخدام Xamarin. الخلاصة منصّة Xamarin واعدة، ولها تاريخ عريق وتجارب غنيّة من قبل أن تظهر شركة Xamarin إلى الوجود. ولعلّ مايكروسوفت قد أدركت الأهميّة الكبيرة لها، فتمّت عمليّة الاستحواذ التي كانت متوقعّة. أنصحك بأن تبادر إلى تعلّم Xamarin وخصوصًا في حال كنت مبرمج سي شارب، أو لديك معلومات أوليّة عنها. ولعلّ الأيّام القادمة قد تحمل المزيد من الدعم والمفاجآت لمنصّة Xamarin التي كان أوّلها هو جعلها مجّانيّة للاستخدام الشخصيّ أو للفرق البرمجيّة الصغيرة. سنبدأ اعتبارًا من الدرس القادم في هذه السلسلة التعليميّة، تنصيب برنامج Visual Studio 2015 وبدء العمل مع Xamarin.
  7. بمجرد حصولي على ساعة آبل Apple Watch، كنت قد رسلت للجميع رموزًا تعبيرية منذ اليوم الأول، لقد كنت مسحورًا بها. شعرت أيضًا بالإلهام كمصمم. لم أصمم من قبل لشاشات صغيرة جدًا، وقد جذبتني فكرة صنع شيء في مساحة تملك مجموعة فريدة من الحدود والميزات. قررت صنع تطبيق لساعة آبل كمشروع ثانوي، و كنقطة انتقال لتصميمي. نظرت لتطبيق الهاتف الذي استعمله لمراقبة استهلاك الكهرباء الخاص بي ودفع فاتورة البلدية الشهرية، بدى كأن بعض ميزات هذا التطبيق بإمكانها بالفعل أن تعمل بشكل جيد على الساعة. مقارنة استهلاك الأشهر، وإضافة دفعات، ومهام أخرى تُعرض بسهولة على الهاتف أو الجهاز اللوحي قد تشكل تحديًا مثيرًا عند عرضها على الساعة. وها هي نظرة عن قرب على أول محاولاتي في صنع نموذج لشاشة صغيرة جدًا. معالجة الوحش الصغير جدًا إن قمت من قبل بتصميم تطبيق iOS أو Android، فقد لاحظتَ بأنه من المهم تذكر تركيبهم المعروف وأخذه بالحسبان. مثلًا: علامات التبويب في الأعلى بالنسبة لـAndroid وفي الأسفل بالنسبة لـiOS. هكذا يتوقع المستخدمون أن يجدوهم. لا يمكنك إعادة صنع تطبيق هاتف محمول لساعة آبل. يجب أن يكون تطبيق الساعة امتدادًا لتطبيق الهاتف، كصديقٍ صغير الشاشة يساعد في تحسين تجربة الهاتف. مثل iOS و Android، ساعة آبل لديها نوعها الخاص من اللغة البصرية وطرقها التي يفترض على المستخدم أن يجربها. أردتُ أن أكون صادقًا بتجربة ساعة آبل، لذلك قرأت بعناية عبر مستندات تصميم آبل. أردت أن أعرف أي نوع من الانتقالات والتحكمات كانت تتميز بها ساعة آبل، وكل النواحي التقنية التي أحتاج لمعرفتها. خلال بحثي، اكتشفت بأن شركة آبل لديها انتقالات وأنماط تنسيق معينة ومصممة لمنصة الساعة. تمّ تصميم الأنماط مع التفكير المعمق في السيناريوهات التي سوف تستخدم فيها. تفصيل سريع: اللمحات السريعة: مجموعات قابلة للتصفح من اللحظات الوقتية والسياقية من التطبيقات المفضلة للمستخدم. نظرات قصيرة: إشعارات تظهر بشكل سريع وتعطي المستخدم وقتًا كافيًا لرؤية عن ماذا يدور الإشعار وأي تطبيق قام بإرساله. نظرات طويلة: تزوّد تفاصيل أكثر عن إشعار قادم. تظهر النظرة الطويلة عندما يبقى معصم المستخدم مرفوعًا أو عندما ينقر المستخدم على النظرة القصيرة. الانتقال القائم على الصفحة في مواجهة الانتقال الهرمي: يعتمد الانتقال القائم على الصفحة على السحب أو التحريك بشكل أفقي خلال الصفحات. لديه نفس السلسلة الأفقية من النقاط التي تراها في أسفل الصفحة الرئيسية في هاتف iPhone. أما الانتقال الهرمي فهو طريقة للنزول للأسفل للوصول للمعلومات. نقرك على عنصر قائمة رئيسية يأخذك للصفحة التالية. هنالك الكثير من الفروق البسيطة بين هذه التراكيب: اللمحة السريعة، والنظرة القصيرة، والنظرة الطويلة، والانتقال الهرمي والانتقال الأفقي. ومثل الهاتف المحمول، هنالك لحظات حيث تكون كل من هذه التراكيب مناسبةً، وإن استخدمتها في السياق المناسب فسوف يتعامل معها المستخدم بسهولة وبشكل بديهي. خلال محاولة فهم كيفية الانتقال في الأجهزة القابلة للارتداء، نصيحتي لك كالتالي: لا تقم بإعادة اختراع العجلة! أي لا تقم بمحاولة التصميم من الصفر. هذه الأنواع من الانتقالات وشاشات البيانات تم تصميمها لسيناريوهات معينة. اكتشف كيف بإمكانها التعامل مع المعلومات التي تحتاج أن توصلها للمستخدم. البحث بمجرد وصولي للفهم الأساسي لكيفية عرض ساعة آبل للمعلومات، بدأت التجريب بكيف يمكن تصوير او تجسيد البيانات في السيناريوهات المعروفة. على سبيل المثال، أي نوع من المعلومات يتم تجاهلها، بالمقابل أي المعلومات يتم ملاحظتها؟ لقد أضفت العديد من تطبيقات الساعة لكي أستطيع العبث بها ورؤية كيف قام الآخرون بحل مزايا الانتقال من الهاتف المحمول للصيغة الجديدة على الساعة. الجولة الأولى اعتقدت بالبداية بأنني أريد أن أصنع انتقالًا هرميًّا، باعتباره الطريقة التي وُضع فيها تطبيق الهاتف المحمول. وبالرغم من أنه ساهم في عمل تطبيق متكامل، أدركت بأنه لم يكن يعمل بالطريقة التي أردتها، فكان ينقصه الحياة والبهجة. بدى وكأنه مثقل بالنص على عكس كونه قادرًا على عرض المعلومات بسرعة. أدركت بأنه يجب علي أن أتبنى "رؤية آبل" في ساعة آبل: عرض رائع للبيانات بدون وجود الكثير من النص. قمت بعرض النموذج الأولي على فريق التصميم، حيث علمت بأن هناك تغييرات يجب عملها. حصلت على الكثير من ردود الفعل: "لا يبدو كثيرًا كتطبيق ساعة. لم لا تجرب أمثلة تصميم مبنية على البيانات؟" لقد كان فريق التصميم على حق. لذلك بدأت من جديد. عليك أحيانًا أن تفكك كل شيء وتبدأ من الصفر كنت قد اصبحت تائهًا في رسم تخطيط واحد لواحد (1:1 mapping) لمزايا من التطبيق للساعة الذكية، ولم أتّبع خطوات عمليتي الخاصة. يبدأ المشروع عادةً بقائمة من المزايا أو متطلبات العمل، ومنها أقوم تاليًا بتوليد تدفقات (flows) وإطارات شبكية (wireframes) ثم في النهاية أنتقل إلى التخطيط الشامل Comps (Comprehensive Layout) وإلى النمذجة (prototyping). لم أقم في محاولتي الأولى بتكوين خط أساسي لـMVP (المنتج الأدنى القابل للنمو لجعل تطبيقٍ ما فعالًا ومفيدًا). كل ما أردته هو الفحص ورؤية ما يحدث. وما حدث كان باهتًا وغير واضحًا. لم أكن لأتعامل مع عمل زبون هكذا، لذلك أخدت هذه المرة خطوةً للخلف وخططت منتج MVP. تعاونت مع مطورين لوضع مجموعة MVP حقيقية من الميزات للتصميم. ثم أخذت تلك القائمة، أمسكت بدفتر ملاحظاتي، وبدأت العمل. لدي كلمة بشأن الرسم التخطيطي: أنصح به بشدة كأسلوب حل مشاكل أولي. وأنا لا أتحدث تلك الرسومات المثالية والرائعة الموجودة في دفاتر مولسكين (Moleskines) الجلدية والتي تراها عادةً في منشورات المصممين على مدوناتهم. أنا أتحدث عن الرسومات القبيحة، السريعة والتي يمكن التخلص منها. تلك الرسومات التي تخاف من أن يراها الناس أو من الممكن أن يقولوا: "ظننت بأنك قلت أنك ذهبت لكلية الفنون؟" العمل بسرعة على سبورة بيضاء أو على دفترك يكوّن مجموعة من الميّزات. اجمع البيانات المتشابهة والأعمال سويًا. ارسم إطارات شبكية (wireframes) وتدفقات (flows) بسيطة. ليس من الضروري أن تكون مثالية. وبمجرد أن تستثمر جهدك في جعلها مثالية، ستكون أقل قابلية لأن تستغني عنها أو تتخلص منها وتبدأ من جديد في حال وجدت خطأً ما أو أردت أن تُجري تغييرًا. في النهاية أقوم بإنتاج نسخ جميلة في ما بعد لعرضها على العملاء وعادةً بشكل رقمي. المرور بكل تفصيل صغير يساعدني على رؤية طرق مختلفة لتجميع الميزات سويًا لتطبيق مبني على الصفحة (page-based). أقوم بتحليل وتجزئة الميّزات إلى مهام على شكل حبيبات، ثم أقوم بتجميعها سويًا في شاشات. لقد عملت بشكل حر وسريع لأنه بالأصل، هذه كانت مجرد لائحة بقالة (أداة تساعدني على ترتيب الميزات والمحاسبة عليها). الإلهام لقد كنت مفتونًا بالطريقة التي يستحضر فيها تطبيق الطقس في ساعة آبل واجهة ساعة بوساطة عرض بيانات الطقس بشكل دائري. حاولت أن أوضح بعض قصص المستخدمين سويًا في تنسيق دائري، كالأيام المتبقية حتى موعد دفع الفاتورة. إنّ استخدام التقويم وعرضه بأسلوب دائري أو تحويل شريط مقارنة لتصوير بياني دائري الشكل هو تحدٍ مثير. اغسل بالصابون، اشطف، كرر اعادة التفكير بالتطبيق واعتباره مبنيًا على الصفحة (page-based) مكنني من دمج العديد من القطع المختلفة من البيانات ونقاط الانطلاق المحتملة بشاشة واحدة. مكنني أيضًا من جعله مختصرًا وبصريًا بشدة. جعل التصفح (Paging) خلال تطبيق ساعة ذكية استخدام التطبيق أكثر سرعة وأكثر متعةً في الاستخدام وجعله أيضًا أكثر متعة بالتصميم. الدروس المستفادة أحد أكثر الدروس التي تعلمتها أهمية: انغمس ولا تتردد. ابنِ تصميمك من قصص المستخدمين. التدفق (flow)، الإطار الشبكي (wireframe)، النموذج (prototype). لن تتعلم إلا بهذه الطريقة. إن كنت تقدر على الحصول على ردود فعل من مبدعين آخرين ومن مطوري iOS فقم بطلبها. سوف يساعدوك باكتشاف أشياء تجاهلتها مسبقًا وسوف تصنع نموذجًا أفضل من عملك. بجانب ذلك، فإن امتلاكي لميّزات MVP واقعية ساعدني على تحديد ما احتجت ان أقضي وقتي فيه وما احتجت أن اركز عليه بشكل أفضل. قم بتجربة الأشياء، أعد تقييم عملك ولا تكن خائفًا من ترك شيءٍ ما عندما ترى خللًا. إبقاؤك لنسخة من الشاشات السابقة بإمكانه أن يكون ذو فائدة عندما تحتاج لعرض عملك على الزبائن أو على فريقك. حتى وإن لم يكن هنالك زبون يدفع لك مقابل تصميمك لتطبيق ساعة ذكية، فإن إكمال تصميم كهذا بوصفه تدريبًا سوف يعود عليك بالفائدة في المستقبل. بإمكانه أن يكون منشور على مدونة حول موضوع جديد ومثير يبيّن للزبائن المستقبليين بأنك على إطّلاع على كل ما هو جديد. بإمكانه أن يكون إضافةً رائعة لمعرض أعمالك الذي يُظهر كيف تتعامل مع المشروع. بالإضافة لذلك، فإنه فرصة لنقل مهارات تفكيرك الحساس والذي أراه نادرًا عندما أراجع طلبات التوظيف. بالرغم من أن هذا المشروع لم يكن لزبون، فقد استخدمت هذا التصميم لعرض على الزبون المستقبلي كيف تعاملت مع تجربة المستخدم وكيف قُمتُ بالتصميم باعتبار المنصة والمستخدم وقمت بالتفكير بكيف يمكن لتطبيق ساعة ذكية أن يندمج مع تطبيق هاتف محمول موجود بالفعل. وفي حال كنت تتساءل: لقد حصلت على العمل. ترجمة -بتصرف- للمقال ‎Prototyping for a tiny screen لصاحبه Marc Nischan
  8. يُتيح نظام تشغيل أندرويد طريقة محددة وبسيطة للتنقل بين المكونات الأساسية للتطبيقات والتي ذكرناها سابقًا وذلك عن طريق استخدام الـ Intents. Intents يُعتبر الـ Intent طريقة التواصل بين مكونات التطبيق المختلفة من أجل القيام بشيء ما، فهو يسمح لمكونات التطبيق (كالأنشطة Activities ، الخدمات Services وغيرها) من التفاعل مع بعضها البعض والقيام بإحدى المهام، فباستخدام الـ Intent يمكننا التنقل بين شاشات التطبيق المختلفة أو التطبيقات الأخرى فمثلًا عند الضغط على زر يقوم بفتح نشاط Activity جديد داخل نفس التطبيق وعرض صورة به، كما يسمح بتفاعل التطبيقات الأخرى المتواجدة على الهاتف مع تطبيقك، فيمكن عرض الصورة في تطبيق خارجي كتطبيق المعرض (Gallery). وعند استخدام الـ Intent في التنقل بين مكونات نفس التطبيق يسمى بالـ Explicit Intent، لذا يستخدم Explicit Intent في ربط مكونات التطبيق الداخلية ببعضها وسنتعرف من خلال الأمثلة القادمة على كيفية استخدام الـ Explicit Intents في التنقل بين الأنشطة وإرسال واستقبال البيانات بينهم. وعند استخدامه للتنقل إلى التطبيقات الخارجية يسمى بالـ Implicit Intent. المثال الأول الآن قم بفتح Android Studio وقم بعمل تطبيق جديد اسمه "Explicit Intent Example"، في هذا المثال سنقوم باستخدام زر للتنقل إلى نشاط جديد. أولًا نقوم بصنع واجهة المستخدم الخاصة بالنشاط الأول والرئيسي في الملف activity_main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Welcome to The First Activity" android:textSize="21sp"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="GO!" android:id="@+id/gobtn"/> </LinearLayout> ثانيًا للاستجابة للزر عند الضغط عليه نقوم باستخدام onClickListener. package apps.noby.explicitintentexample; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { Button go; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); go =(Button) findViewById(R.id.gobtn); go.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Create your Intent Here } }); } } وبداخلها سنقوم بكتابة الـ Intent المناسب ولكن قبل ذلك ينبغي أن نقوم بعمل نشاط جديد وربطه بواجهة استخدام جديدة ولعمل ذلك نضغط بالزر الأيمن على المجلد المسمى باسم الحزمة ثم اختر: New > Activity > Empty Activity قم بكتابة اسم النشاط الجديد وتأكد من وجود اسم الحزمة في المكان المخصص لها كما بالصورة التالية: بعد أن يقوم البرنامج بصنع ملفات النشاط الجديد من ملف activity_second.xml سنقوم بصنع واجهة المستخدم الخاصة بالنشاط الثاني. وسنضع بداخلها TextView يعرض النص "Welcome to The Second Activity": <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Welcome to The Second Activity" android:textSize="21sp"/> </LinearLayout> والآن نعود مرة أخرى للنشاط الأول لنكمل التابع ()onClick، ولصنع Intent نقوم بعمل كائن من الصنف Intent وتمرير لدالة البناء الخاصة به النشاط الذي سننتقل منه وهو MainActivity.this وتمرير النشاط الذي نريد الانتقال إليه وهو SecondActivity.class. Intent i = new Intent(MainActivity.this,SecondActivity.class); بهذا السطر نكون قد صنعنا الهدف ولكن يتبقى أن نقوم بتفعيل هذا الهدف ليستجيب له التطبيق وذلك عن طريق التابع ()startActivity وتمرير الـ Intent الذي قمنا بصناعته إليه. لتصبح الشيفرة النهائية هي: package apps.noby.explicitintentexample; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { Button go; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); go =(Button) findViewById(R.id.gobtn); go.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Create your Intent Here Intent i = new Intent(MainActivity.this,SecondActivity.class); startActivity(i); } }); } } والآن نقوم بتجربة التطبيق على المحاكي والتأكد من قيامه بالوظيفة المطلوبة. عند النظر إلى ملف AndroidManifest.xml نجد أنه تم إضافة تعريف لعنصر جديد من النوع Activity، ولاحظ أن بدون تعريف العنصر الجديد لن يعمل التطبيق بشكل سليم ولن يتعرف نظام التشغيل عليه. <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".SecondActivity"></activity> </application> … المثال الثاني سنقوم في هذا المثال بإرسال بيانات من نشاط وعرضها في نشاط آخر. أولًا نقوم بصنع النشاط الجديد الذي سيتم عرض البيانات بداخله. ثانيا نبدأ بصنع واجهة المستخدم في ملف activity_main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Your Name" android:textSize="21sp" android:id="@+id/editt"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send" android:id="@+id/sendbtn"/> </LinearLayout> ثم سنقوم بالاستجابة لضغط الزر كما سبق وبداخل التابع ()onClick يتم تعريف الـ Intent وحفظ البيانات المكتوبة في الـ EditText بداخله ثم استدعاء النشاط الجديد. ولإرسال البيانات داخل الـ Intent يتم ذلك عن طريق التابع ()putExtra ونمرر له عنصرين وهما المفتاح و القيمة. المفتاح هو نص نميز به القيمة التي يتم إرسالها عبر الـ Intent ويُستخدم مرة أخرى لاستعادة البيانات التي أرسلناها. والقيمة هي البيانات التي نرغب في إرسالها. package apps.noby.explicitintentexample; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; public class MainActivity extends Activity { final public static String NAME_TAG = "name"; private Button send; private EditText et; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); send =(Button) findViewById(R.id.sendbtn); et = (EditText)findViewById(R.id.editt); send.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Create your Intent Here Intent i = new Intent(MainActivity.this, SecondActivity.class); String name = et.getText().toString(); i.putExtra(NAME_TAG,name); startActivity(i); } }); } } قمنا بتعريف المفتاح على هيئة نص ثابت وذلك لتسهيل استخدام المفتاح مرة أخرى عند استعادة البيانات في النشاط الآخر. ثم بعد ذلك نقوم بصنع واجهة المستخدم الخاصة بالنشاط الثاني: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, " android:textSize="21sp" android:id="@+id/hellotxt"/> </LinearLayout> ولاستقبال النص الذي أرسلناه بداخل الـ Intent يجب علينا أولًا أن نستعيد الـ Intent الذي قام بفتح النشاط باستخدام التابع ()getIntent. Intent actIntent = getIntent(); ولاستعادة النص الذي حفظناه نستخدم ()getText().toString ونمرر لها المفتاح الذي قمنا بحفظ البيانات به، ثم بعد ذلك نعرض هذا النص بداخل TextView. package apps.noby.explicitintentexample; import android.content.Intent; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class SecondActivity extends AppCompatActivity { private TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); tv =(TextView) findViewById(R.id.hellotxt); Intent actIntent = getIntent(); String name = actIntent.getExtras().getString(MainActivity.NAME_TAG); tv.setText("Hello, " + name); } } ثم نقوم بتجربة التطبيق على المحاكي. المثال الثالث سنقوم في هذا المثال بإعادة البيانات من النشاط الثاني إلى النشاط الأول عند الانتهاء من التفاعل مع النشاط الثاني. أولًا نُنشئ تطبيقًا جديدًا به نشاطان كما في الأمثلة السابقة ونبدأ بصنع واجهة المستخدم الخاصة بالنشاط الأول. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Register" android:id="@+id/regbtn"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="NO Name" android:textSize="30sp" android:id="@+id/regtxt"/> </LinearLayout> وبداخل النشاط الأول نقوم باستدعاء النشاط الثاني عند الضغط على الزر المتواجد بالواجهة ولكن في هذا المثال لإخبار نظام التشغيل بأننا ننتظر بيانات من النشاط الآخر سنقوم بفتح النشاط الآخر بالتابع ()startActivityForResult بدلًا من التابع ()startActivity. private static final int ACTIVITY_REQUEST_CODE = 6 ; private Button reg; private TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); reg =(Button) findViewById(R.id.regbtn); tv = (TextView)findViewById(R.id.regtxt); reg.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Create your Intent Here Intent i = new Intent(MainActivity.this, SecondActivity.class); startActivityForResult(i,ACTIVITY_REQUEST_CODE); } }); } ونمرر للتابع ()startActivityForResult الـ Intent كالمعتاد والرقم الخاص بالطلب وهذا الرقم هو أي رقم صحيح، وفائدته عند رجوع البيانات للنشاط مرة أخرى التمييز بين أي الأنشطة التي قامت بإرسال هذه البيانات. وفي هذا المثال قمنا بتعريف رقم ثابت وتمريره للتابع وعند عودة البيانات سنقوم بالتأكد من هذا الرقم مرة أخرى. والآن نقوم بصنع الواجهة الخاصة بالنشاط الآخر في ملف activity_second.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Enter Your Name" android:textSize="21sp" android:id="@+id/edttxt"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Enter" android:id="@+id/entbtn" android:layout_gravity="right"/> </LinearLayout> بعد ذلك نكتب شيفرة التحكم الخاصة بهذا النشاط. package apps.noby.explicitintentexample; import android.content.Intent; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; public class SecondActivity extends Activity { public static final String REG_NAME_TAG ="name" ; private EditText et; private Button enter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); et =(EditText) findViewById(R.id.edttxt); enter = (Button) findViewById(R.id.entbtn); enter.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String str = et.getText().toString(); Intent in = new Intent(); in.putExtra(REG_NAME_TAG,str); setResult(RESULT_OK,in); finish(); } }); } } فعند الضغط على الزر Enter نقوم بتسجيل النص المتواجد بداخل العنصر EditText، إنشاء Intent جديد وحفظ النص بداخله ثم استدعاء التابع ()setResult والذي يمكننا من إرسال البيانات التي قمنا بتخزينها داخل Intent إلى من قام باستدعاء هذا النشاط. ونمرر للتابع ()setResult الثابت RESULT_OK والمعرّف داخل الـ (API (android.app.Activity كما نمرر لها الـ Intent الذي قمنا بإنشائه وحفظ البيانات بداخله. أخيرًا نستخدم التابع ()finish وهو المسؤول عن غلق النشاط. لاستقبال هذه البيانات في النشاط الأول نقوم بتضمين التابع ()onActivityResult في النشاط كما يلي. package apps.noby.explicitintentexample; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; public class MainActivity extends Activity { private static final int ACTIVITY_REQUEST_CODE = 6 ; private Button reg; private TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); reg =(Button) findViewById(R.id.regbtn); tv = (TextView)findViewById(R.id.regtxt); reg.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Create your Intent Here Intent i = new Intent(MainActivity.this, SecondActivity.class); startActivityForResult(i,ACTIVITY_REQUEST_CODE); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if ( (resultCode == RESULT_OK) && (requestCode == ACTIVITY_REQUEST_CODE)){ String name = data.getExtras().getString(SecondActivity.REG_NAME_TAG); tv.setText(name); } } } وبداخل هذا التابع نتأكد من أن النشاط السابق تم إغلاقه بشكل صحيح ولم يحدث له مشكلة عن طريق التأكد من أن المتغير resultCode يساوي RESULT_OK التي قمنا بتمريرها للتابع ()setResult ثم نتأكد أيضًا من أن النشاط الذي يُعيد هذه البيانات له رقم طلب يساوي رقم الطلب الذي قمنا باستخدامه مسبقًا، بعد ذلك نبدأ في استقبال البيانات. ثم نقوم بتشغيل التطبيق على المحاكي. الأمثلة السابقة كانت باستخدام Explicit Intent للقيام بالوظيفة المطلوبة ولربط مكونات التطبيق المختلفة ببعضها فيمكنك من صنع تطبيق متعدد الأنشطة، في الأمثلة القادمة سنقوم باستخدام الـ Implicit Intents والتعامل معها. المثال الرابع في هذا المثال سنقوم بفتح موقع أكاديمية حسوب في تطبيق المتصفح، وبنفس الطريقة يمكن فتح الروابط الأخرى خارج التطبيق. قم بإنشاء تطبيق جديد يُدعى "Implicit Intent Example" ثم نبدأ بصنع واجهة المستخدم في ملف activity_main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Open Hsoub Academy" android:id="@+id/opnbtn"/> </LinearLayout> وعند الضغط على الزر نقوم بفتح الرابط. Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("http://academy.hsoub.com")); startActivity(i); ونمرر لدالة البناء الخاصة بالـ Intent نوع الفعل الذي نريده من التطبيق الخارجي وفي هذا المثال نريد تطبيق يستطيع عرض البيانات بداخله، ثم نمرر له نوع البيانات المراد عرضها وهي الرابط الخاص بموقع أكاديمية حسوب. لتصبح الشيفرة النهائية الخاصة بالبرنامج كما يلي: package apps.noby.explicitintentexample; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends Activity { private Button open; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); open = (Button) findViewById(R.id.opnbtn); open.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Create your Intent Here Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("http://academy.hsoub.com")); startActivity(i); } }); } } ثم نقوم بتجربة التطبيق النهائي على المحاكي. المثال الخامس في هذا التطبيق سنقوم بالاتصال من خلال فتح تطبيق الاتصال من داخل التطبيق الخاص بنا. نبدأ أولًا بصنع واجهة المستخدم في ملف activity_main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Dial Number" android:id="@+id/dialbtn"/> </LinearLayout> وعند الضغط على الزر نقوم بالاتصال بالرقم التالي: Intent i = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:0123456789")); startActivity(i); ونمرر لدالة البناء الخاصة بالـ Intent نوع الفعل الذي نريده وهو الاتصال بالرقم، ثم نمرر له الرقم. ثم نقوم بتجربة التطبيق النهائي على المحاكي. بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  9. يتميز نظام تشغيل أندرويد بوجود شريط أعلى الشاشة يمُد المستخدم بالعديد من المعلومات الهامة من ضمنها الإشعارات التي ترسلها التطبيقات. فالإشعارات هي جزء من واجهة المستخدم لكنها تظهر خارج التطبيق لإعلام المستخدم بحدث معين، مما يُمكّن المستخدم من عرضه والتفاعل معه بينما يستخدم تطبيقًا آخر. ويحتوي الإشعار على رسالة مختصرة عن هذا الحدث. ويظهر الإشعار عند حدوثه كأيقونة تعبر عن التطبيق في شريط الحالة أعلى الشاشة، ويمكنك معرفة تفاصيله عند سحب درج الإشعارات والضغط على الإشعار. هناك عدة خطوات لصنع الإشعارات من داخل تطبيقك: صنع كائن البناء الخاص بالإشعار من الصنف Notification.Builder Notification.Builder mBuilder = new Notification.Builder(this); من خلال هذا الكائن يمكننا التحكم في الخصائص الخاصة بالإشعار مثل عنوان الإشعار، الأيقونة المستخدمة، التحكم في الأولوية، التحكم في المهام التي يستطيع القيام بها عند الضغط عليه وغيرهم من الخصائص المختلفة. تخصيص الحد الأدنى من الخصائص للإشعار بعد الحصول على كائن البناء نبدأ في التحكم في خصائص الإشعار، وهناك بعض الخصائص الأساسية اللازم توافرها في الإشعار وهي: الأيقونة الخاصة بالإشعار. عنوان الإشعار. المحتوى الخاص بالإشعار. mBuilder.setSmallIcon(R.drawable.msg_icon); mBuilder.setContentTitle("New Message"); mBuilder.setContentText("Hi, This is the message text."); ويوجد العديد من الخصائص الأخرى التي يمكنك استخدامها حسب حاجة التطبيق لها. بناء الإشعار بالخصائص السابقة يتم ذلك باستدعاء التابع ()build والحصول على كائن من Notification. Notification notif = mBuilder.build(); إظهار الإشعار في درج الإشعارات يوفر أندرويد الصنف NotificationManager لإدارة الإشعارات من حيث إصدار الإشعار، تعديل الإشعار بعد إصداره أو إلغاء الإشعار برمجيًا. لذا يجب أولًا الحصول على كائن من هذا الصنف وذلك عن طريق استدعاء الدالة ()getSystemService وتمرير الثابت NOTIFICATION_SERVICE والذي يعني طلب الخدمات الخاصة بالإشعارات من خدمات النظام. وسيتم استخدام الدالة ()getSystemService كثيرًا عند الحاجة لطلب خدمات من النظام. NotificationManager notifyMngr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); بعد ذلك نستدعي التابع ()notify باستخدام الكائن من NotificationManager ليظهر الإشعار في الحال، وتمرير رقم مميز له يُعبر عن الإشعار ليُستخدم هذا الرقم فيما بعد للتعديل على الإشعار أو إلغائه، كما يتم تمرير الإشعار الذي تم بنائه من قبل. int notificationId = 103; notifyMngr.notify(notificationId, notif); تطبيق 1 سنقوم في هذا التطبيق بتجربة الخطوات السابق شرحها لتكوين التطبيق مثل الصورة التالية: أولًا نبدأ بصنع واجهة المستخدم البسيطة في ملف activity_main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Notification" android:id="@+id/shw_notification"/> </LinearLayout> بعد ذلك ننتقل إلى الشيفرة الخاصة بالتطبيق في MainActivity.java باتباع الخطوات ذاتها لصنع الإشعار عند الضغط على الزر. package apps.noby.simplenotification; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { private Button shwbtn; private String title; private String detailText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); shwbtn = (Button) findViewById(R.id.shw_notification); title = "New Message"; detailText ="Hi, This is the message text."; shwbtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Step 1 Notification.Builder mBuilder = new Notification.Builder(MainActivity.this); //Step 2 mBuilder.setSmallIcon(R.drawable.ic_message); mBuilder.setContentTitle(title); mBuilder.setContentText(detailText); //Step 3 Notification notif = mBuilder.build(); //Step 4 NotificationManager notifyMngr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); int notificationId = 103; notifyMngr.notify(notificationId,notif); } }); } } ثم نقوم بتجربة التطبيق على المحاكي للتأكد من عمله كما ينبغي. تطبيق 2 في التطبيق السابق عند الضغط على الإشعار لا يحدث شيئ، لذا في هذا التطبيق سنقوم بفتح نشاط جديد يعرض تفاصيل الإشعار أو ما نريد من معلومات عند الضغط على الإشعار. ولفتح نشاط جديد داخل التطبيق سنستخدم Intent ولكن سنقوم بتغليف الكائن من Intent داخل كائن آخر من PendingIntent وفائدة هذا التغليف هو أنه لا يمكن استخدام الكائن من Intent خارج التطبيق، والإشعار كما ذكرنا يُعرض خارج حدود التطبيق لذا يقوم PendingIntent بإعطاء الصلاحية للتطبيقات الأخرى أو النظام والذي نُرسل إليه PendingIntent القدرة على تنفيذ الأوامر كأنها تتم داخل تطبيقك. أولًا نقوم بصنع نشاط جديد يُدعى ResultActivity. ثانيًا سنقوم بتعديل الشيفرة الخاصة بـ MainActivity.java وإضافة كائن من Intent. Intent intent = new Intent(MainActivity.this,ResultActivity.class); ونستطيع إرسال بيانات إلى النشاط الآخر بنفس الطريقة المستخدمة في الدروس السابقة. intent.putExtra(DESC_KEY,detailText); ثم نقوم بتغليف الكائن داخل PendingIntent: PendingIntent pIntent = PendingIntent.getActivity(MainActivity.this , 0 , intent , PendingIntent.FLAG_UPDATE_CURRENT); والخطوة الأخيرة هي وضع هذا Intent ضمن خصائص الإشعار باستخدام كائن البناء: mBuilder.setContentIntent(pIntent); لتُصبح الشيفرة النهائية لملف MainActivity.java بعد التعديل هي: package apps.noby.simplenotification; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { private Button shwbtn; private String title; private String detailText; public final static String DESC_KEY ="descriptionKey"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); shwbtn = (Button) findViewById(R.id.shw_notification); title = "New Message"; detailText ="Hi, This is the message text."; shwbtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Step 1 Notification.Builder mBuilder = new Notification.Builder(MainActivity.this); //Step 2 mBuilder.setSmallIcon(R.drawable.ic_message); mBuilder.setContentTitle(title); mBuilder.setContentText(detailText); Intent intent = new Intent(MainActivity.this,ResultActivity.class); intent.putExtra(DESC_KEY,detailText); PendingIntent pIntent = PendingIntent.getActivity(MainActivity.this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent); //Step 3 Notification notif = mBuilder.build(); //Step 4 NotificationManager notifyMngr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); int notificationId = 103; notifyMngr.notify(notificationId,notif); } }); } } ولم نقم بتغيير شيء في ملف الواجهة activity_main.xml. الآن لعرض التفاصيل التي سترسل إلى النشاط الجديد ResultActivity.java نبدأ في صنع واجهة المستخدم الخاصة بالنشاط ثم بتغيير الشيفرة الخاصة به ولا يوجد اختلاف بينها وبين الطريقة المستخدمة في الدروس السابقة. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="25sp" android:id="@+id/desc"/> </LinearLayout> package apps.noby.simplenotification; import android.content.Intent; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class ResultActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_result); TextView tv = (TextView) findViewById(R.id.desc); Intent intent = getIntent(); String description = intent.getExtras().getString(MainActivity.DESC_KEY); tv.setText(description); } } ثم نقوم بتجربة التطبيق على المحاكي. تطبيق 3 لاحظ أنه عند الضغط على الإشعار في التطبيق السابق يقوم بفتح نشاط جديد ولكن يظل الإشعار متواجدًا في درج الإشعارات رغم عرضه والتفاعل معه لذا سنقوم في هذا التطبيق بتغيير بسيط حتى يختفي الإشعار بمجرد التفاعل معه. في شيفرة التطبيق السابق بداخل الملف MainActivity.java سنقوم بتعديل السطور الخاصة بالخطوة الثانية لتُصبح. mBuilder.setContentTitle(title); mBuilder.setContentText(detailText); mBuilder.setAutoCancel(true); Intent intent = new Intent(MainActivity.this,ResultActivity.class); intent.putExtra(DESC_KEY,detailText); قمنا فقط بإضافة السطر الخاص باستدعاء التابع ()setAutoCancel وتمرير القيمة true باستخدام كائن البناء، والآن عند تجربة التطبيق بعد هذا التعديل ستجد أنه يقوم بإزالة الإشعار بمجرد الضغط عليه. تطبيق 4 يتم استخدام الشكل السابق بكثرة خاصة في تطبيقات المحادثة أو رسائل البريد حيث يتم عرض صورة المستخدم وبجانبها بحجم صغير الأيقونة الخاصة بالتطبيق. ولصنع ذلك يتم استدعاء التابع ()setLargeIcon باستخدام كائن البناء وتمرير له الصورة المراد عرضها بحجم كبير. لكن هناك شرط وهو أن تكون الصورة بصيغة Bitmap ولأننا حتى الآن نقوم في الدروس باستخدام الصور المتواجدة في مجلد drawable لذا ينبغي قبل تمريرها للتابع ()setLargeIcon أن نقوم بتحويلها إلى Bitmap ويتم ذلك عن طريق الخطوة التالية. Bitmap img = BitmapFactory.decodeResource(MainActivity.this.getResources(), R.drawable.ic_person); ثم بعد ذلك تمريرها كما سبق لبناء الإشعار. mBuilder.setLargeIcon(img); لتُصبح الشيفرة الكاملة الخاصة بالملف MainActivity.java هي: package apps.noby.simplenotification; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { private Button shwbtn; private String title; private String detailText; public final static String DESC_KEY ="descriptionKey"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); shwbtn = (Button) findViewById(R.id.shw_notification); title = "New Message"; detailText ="Hi, This is the message text."; shwbtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Step 1 Notification.Builder mBuilder = new Notification.Builder(MainActivity.this); //Step 2 mBuilder.setSmallIcon(R.drawable.ic_message); Bitmap img = BitmapFactory.decodeResource(MainActivity.this.getResources(), R.drawable.ic_person); mBuilder.setLargeIcon(img); mBuilder.setContentTitle(title); mBuilder.setContentText(detailText); mBuilder.setAutoCancel(true); Intent intent = new Intent(MainActivity.this,ResultActivity.class); intent.putExtra(DESC_KEY,detailText); PendingIntent pIntent = PendingIntent.getActivity(MainActivity.this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent); //Step 3 Notification notif = mBuilder.build(); //Step 4 NotificationManager notifyMngr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); int notificationId = 103; notifyMngr.notify(notificationId,notif); } }); } } ليعمل التطبيق كما هو متوقع على المحاكي. تطبيق 5 في بعض الأحيان نحتاج إلى التفاعل السريع للمستخدم مع التطبيق دون الحاجة لفتح التطبيق أو لتوفير أكثر من اختيار لفتح أجزاء محددة في التطبيق. للقيام بذلك نستخدم التابع ()addAction والذي يمكننا من إضافة زر إلى الإشعار. PendingIntent pIntent = PendingIntent.getActivity(MainActivity.this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent); mBuilder.addAction(R.drawable.ic_reply,"Reply",pIntent); mBuilder.addAction(R.drawable.ic_content_copy,"Copy",pIntent); //Step 3 Notification notif = mBuilder.build(); ولاستخدام هذا التابع نقوم بتمرير صورة لتظهر داخل الزر، النص الخاص بالزر وأخيرًا كائن من PendingIntent ليتم تنفيذه عند الضغط على هذا الزر. ويمكنك عمل ()addAction حتى ثلاث مرات فقط، ويمكن لكل زر أن يكون له PendingIntent مختلف خاص به ليقوم بتنفيذ أمر مختلف وفي المثال السابق لشرح الفكرة تم استخدام PendingIntent واحد. والآن نقوم بتشغيل التطبيق على المحاكي للتأكد من عمله بشكل صحيح. تطبيق 6 في بعض الأحيان تحتاج إلى التعديل على إشعار سابق دون إصدار إشعار جديد، وذلك بإضافة بعض المعلومات له أو بتغيير محتوى الإشعار أو كلاهما. وقد تحتاج أيضًا إلى إزالة الإشعار برمجيًا دون تدخل من المستخدم وذلك عند حدوث شيء محدد أو مرور وقت محدد. أولًا للقيام بالتعديل أو بتغيير محتوى إشعار دون إصدار إشعار آخر جديد نقوم ببناء الإشعار ثم نقوم بإصداره باستخدام نفس الـ NotificationID الذي نمرره للتابع ()notify حتى يقوم بتعديل الإشعار صاحب ID ذاته. بفرض أن لدينا هذا الإشعار عند الضغط على زر Show Notification. shwbtn = (Button) findViewById(R.id.shw_notification); shwbtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Step 1 Notification.Builder mBuilder = new Notification.Builder(MainActivity.this); //Step 2 mBuilder.setSmallIcon(R.drawable.ic_message); Bitmap img = BitmapFactory.decodeResource(MainActivity.this.getResources(), R.drawable.ic_person); mBuilder.setLargeIcon(img); mBuilder.setContentTitle("New Message"); mBuilder.setContentText("Hi, This is the message text."); mBuilder.setNumber(++totalNumber); mBuilder.setAutoCancel(true); Intent intent = new Intent(MainActivity.this,ResultActivity.class); intent.putExtra(DESC_KEY,"Hi, This is the message text."); PendingIntent pIntent = PendingIntent.getActivity(MainActivity.this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent); //Step 3 Notification notif = mBuilder.build(); //Step 4 NotificationManager notifyMngr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); int notificationId = 103; notifyMngr.notify(notificationId,notif); } }); ثم عند الضغط على الزر Update Notification يتم تغييرها إلى المحتوى التالي. updatebtn = (Button) findViewById(R.id.upd_notification); updatebtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Step 1 Notification.Builder mBuilder = new Notification.Builder(MainActivity.this); //Step 2 mBuilder.setSmallIcon(R.drawable.ic_message); Bitmap img = BitmapFactory.decodeResource(MainActivity.this.getResources(), R.drawable.ic_person); mBuilder.setLargeIcon(img); mBuilder.setContentTitle("Updated Message"); mBuilder.setContentText("Hi, This is an updated Message."); mBuilder.setNumber(++totalNumber); mBuilder.setAutoCancel(true); Intent intent = new Intent(MainActivity.this,ResultActivity.class); intent.putExtra(DESC_KEY,"Hi, This is an updated Message."); PendingIntent pIntent = PendingIntent.getActivity(MainActivity.this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent); //Step 3 Notification notif = mBuilder.build(); //Step 4 NotificationManager notifyMngr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); int notificationId = 103; notifyMngr.notify(notificationId,notif); } }); لاحظ أننا قمنا باستخدام نفس NotificationID في خطوة إظهار الإشعار وذلك لإخبار النظام أننا نريد التعديل على إشعار متواجد بالفعل. والآن لإزالة الإشعار نضغط على الزر Cancel Notification. cnclbtn = (Button) findViewById(R.id.cncl_notification); cnclbtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { NotificationManager notifyMngr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); totalNumber = 0; int notificationId = 103; notifyMngr.cancel(notificationId); } }); لإزالة الإشعار نستدعي التابع ()cancel باستخدام الكائن من NotificationManager ونمرر لها الـ ID الخاص بالإشعار. لاحظ أننا قد قمنا باستخدام التابع ()setNumber عند بناء الإشعار وذلك ليُظهر عدد مرات بناء وتعديل الإشعار ذاته، ويتم بدأ العد مجددًا عند الضغط على الزر Cancel Notification. وبتغيير ملف الواجهة في activity_main.xml ليُصبح. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Notification" android:id="@+id/shw_notification"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Update Notification" android:id="@+id/upd_notification"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Cancel Notification" android:id="@+id/cncl_notification"/> </LinearLayout> تستطيع أن تلاحظ الرقم المتواجد أقصى يمين الإشعار والذي يُعبر عن عدد مرات التعديل على الإشعار ذاته، ويتحكم التابع ()setNumber في القيمة التي ستظهر بناءً على قيمة المعامل الذي تم تمريره له. تطبيق 7 هناك العديد من الإشعارات التي لا يستطيع المستخدم من إزالتها ويجب عليه انتظار انتهاء حدث معين أو القيام بمهمة معينة حتى يختفي الإشعار، مثال على ذلك عند تحميلك لملف تجد إشعار لا يمكنك إزالته يُخبرك بمعلومات عن التحميل وللتخلص منه ينبغي الانتظار حتى اكتمال تحميل الملف أو إلغاء التحميل. لتفعيل هذه النوعية من الإشعارات داخل التطبيق لدينا طريقتان 1. نقوم بتغير قيمة إحدى خصائص الإشعار الذي قمنا ببنائه وتدعى flags، حيث نقوم بإضافة خاصيتين إلى الإشعار وهما: Notification.FLAG_NO_CLEAR وهي تلغي تأثير التابع ()setAutoCancel فلا يتم مسح الإشعار عند الضغط عليه. Notification.FLAG_ONGOING_EVENT وهي المسؤولة عن إخبار النظام أن هذا الإشعار لديه معلومات لا زالت لها أهمية ولا ينبغي إزالتها الآن. //Step 3 Notification notif = mBuilder.build(); notif.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; 2. استدعاء التابع ()setOngoing باستخدام كائن البناء و تمرير له القيمة true، مع إزالة سطر استدعاء التابع ()setAutoCancel حتى لا يتم إزالة الإشعار عند الضغط عليه. //Step 2 mBuilder.setSmallIcon(R.drawable.ic_message); mBuilder.setContentTitle("New Message"); mBuilder.setContentText("Hi, This is the message text."); mBuilder.setOngoing(true); ملحوظة: لا يوجد فرق بين أي من الطريقتين من حيث الوظيفة أو التأثير. ولإزالة هذا الإشعار عند الانتهاء من الحدث ينبغي استدعاء التابع ()cancel وتمرير له NotificationID كما سبق، ولا يمكن إزالته إلا بهذه الطريقة. تطبيق 8 عندما يأتي تنبيه من تطبيق ما وكانت شاشة القفل مغلقة فهناك ثلاثة اختيارات للنظام للتعامل مع هذا الإشعار. إما إظهار محتوى الإشعار على شاشة القفل، أو إظهار الإشعار مع إخفاء محتواه أو عدم إظهار الإشعار ويمكن التحكم في هذه الاختيارات برمجيًا من خلال التابع ()setVisibility باستخدام كائن البناء ويُمرر له ثابت قيمته: Notification.VISIBILITY_PUBLIC إذا أردت أن يظهر محتوى الإشعار. Notification.VISIBILITY_PRIVATE إذا أردت إظهار الإشعار فقط دون إظهار محتواه. Notification.VISIBILITY_SECRET وذلك لعدم ظهور الإشعار على شاشة القفل ويظهر حين نتجاوز قفل الشاشة أولًا. //Step 2 mBuilder.setSmallIcon(R.drawable.ic_message); mBuilder.setContentTitle("New Message"); mBuilder.setContentText("Hi, This is the message text."); mBuilder.setAutoCancel(true); mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); ملحوظة: حتى يعمل التابع ()setVisibility يجب أن يكون الحد الأدنى من نسخة النظام التي يدعمها التطبيق هي Lollipop API 21، حيث لا تدعم النسخ الأقدم هذه الميزة لذا تأكد من تغييرها عند عمل مشروع جديد. تطبيق 9 يدعم أندرويد عدة أنواع من الإشعارات بجانب النوع الأساسي الذي استخدمناه في الأمثلة السابقة. Big Text Style حيث تستطيع أن تجمع بين الأمرين، إشعار بسيط يعرض نصًا مختصرًا ويمكن للمستخدم أن يقوم بتكبيره لعرض المزيد من التفاصيل. ويكثر استخدام هذه الطريقة عند عرض إشعار برسالة بريد إلكتروني جديدة فيمكن لك تكبيرها وقراءة تفاصيل الرسالة دون فتح التطبيق. ويمكنك تطبيق ذلك في تطبيقك عن طريق. عمل إشعار وتحديد خصائصه كما سبق وذلك لتُعرض هذه المعلومات عندما يكون الإشعار صغير الحجم. عمل كائن من Notification.BigTextStyle وتحديد عنوان جديد وعرض تفاصيل أكثر ثم إضافته لخصائص الإشعار. بناء الإشعار. ولتوضيح هذه الخطوات بالشيفرة نقوم بالتالي: private void showBigTextNotification() { //Step 1 mBuilder = new Notification.Builder(this); mBuilder.setSmallIcon(R.drawable.ic_txt); mBuilder.setContentTitle(“Big Text in Normal Mode”); mBuilder.setContentText(“Example of a Big Text Notification.”); //Step 2 Notification.BigTextStyle bigText = new Notification.BigTextStyle(); bigText.bigText(“This Text will be repeated over and over.This Text will be repeated over and over.This Text will be repeated over and over.This Text will be repeated over and over.”); bigText.setBigContentTitle(“Big Text in Expansion Mode”); mBuilder.setStyle(bigText); //Step 3 Notification notfi = mBuilder.build(); mNotificationManager.notify(200,notfi); } Inbox Style يختلف هذا النوع عن النوع السابق في أنه يتيح لك عرض التفاصيل في أكثر من سطر، أما النوع السابق سيعرض كافة التفاصيل في نفس السطر دون القدرة على التحكم فيما يُعرض في كل سطر على حدا. ويُستخدم هذا النوع عند التعديل على إشعار وإضافة المزيد من التفاصيل إليه. مثل تطبيقات البريد الإلكتروني عند عرض في نفس الإشعار سطرًا عن كل رسالة قادمة. لتطبيق هذا النوع نمر بنفس الخطوات السابقة ولكن باستخدام كائن من الصنف Notification.InboxStyle. لاحظ أن لكل صنف توابعه الخاصة التي تُمكنك من إضافة الخصائص له. private void showInboxNotification() { //Step 1 mBuilder = new Notification.Builder(this); mBuilder.setSmallIcon(R.drawable.ic_inbox); mBuilder.setContentTitle(“Inbox in Normal Mode”); mBuilder.setContentText(“Example of a Inbox Notification.”); lines = new String[6]; lines[0] = “Line number 1”; lines[1] = “Line number 2”; lines[2] = “Line number 3”; lines[3] = “Line number 4”; lines[4] = “Line number 5”; lines[5] = “Line number 6”; //Step 2 Notification.InboxStyle inbox = new Notification.InboxStyle(); for(int I = 0 ; i<lines.length; i++) inbox.addLine(lines[i]); inbox.setBigContentTitle(“Inbox in Expansion Mode”); mBuilder.setStyle(inbox); //Step 3 Notification notfi = mBuilder.build(); mNotificationManager.notify(97,notfi); } Big Picture Style عندما تكون تفاصيل الإشعار هي صورة يُفضل استخدام هذا النوع. وأيضًا يختلف نوع الكائن المستخدم Notification.BigPictureStyle. private void showBigPictureNotification() { //Step 1 mBuilder = new Notification.Builder(this); mBuilder.setSmallIcon(R.drawable.ic_picture); mBuilder.setContentTitle(“Big Picture in Normal Mode”); mBuilder.setContentText(“Example of a Big Picture Notification.”); Bitmap img = BitmapFactory.decodeResource(getResources(),R.drawable.picture); //Step 2 Notification.BigPictureStyle bigPic = new Notification.BigPictureStyle(); bigPic.bigPicture(img); bigPic.setBigContentTitle(“Big Picture in Expansion Mode”); mBuilder.setStyle(bigPic); //Step 3 Notification notfi = mBuilder.build(); mNotificationManager.notify(6,notfi); } وبدمج كافة الأنواع في تطبيق واحد يعرض الإشعار عند الضغط على الزر المناسب لتُصبح الشيفرة النهائية لملف MainActivity.java هي: package apps.noby.advancednotification; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { private Button bigTextButton; private Button inboxButton; private Button bigPictureButton; private Button cancelButton; private Notification.Builder mBuilder; private NotificationManager mNotificationManager; private String[] lines; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bigTextButton = (Button) findViewById(R.id.bg_txt); inboxButton = (Button) findViewById(R.id.inbx); bigPictureButton = (Button) findViewById(R.id.bg_pic); cancelButton = (Button) findViewById(R.id.cncl); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); bigTextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v){ showBigTextNotification(); } } ); inboxButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showInboxNotification(); } }); bigPictureButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showBigPictureNotification(); } }); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { cancelNotification(); } }); } private void cancelNotification() { mNotificationManager.cancelAll(); } private void showBigTextNotification() { mBuilder = new Notification.Builder(this); mBuilder.setSmallIcon(R.drawable.ic_txt); mBuilder.setContentTitle("Big Text in Normal Mode"); mBuilder.setContentText("Example of a Big Text Notification."); Notification.BigTextStyle bigText = new Notification.BigTextStyle(); bigText.bigText("This Text will be repeated over and over.This Text will be repeated over and over.This Text will be repeated over and over.This Text will be repeated over and over."); bigText.setBigContentTitle("Big Text in Expansion Mode"); mBuilder.setStyle(bigText); Notification notfi = mBuilder.build(); mNotificationManager.notify(200,notfi); } private void showInboxNotification() { mBuilder = new Notification.Builder(this); mBuilder.setSmallIcon(R.drawable.ic_inbox); mBuilder.setContentTitle("Inbox in Normal Mode"); mBuilder.setContentText("Example of a Inbox Notification."); lines = new String[6]; lines[0] = "Line number 1"; lines[1] = "Line number 2"; lines[2] = "Line number 3"; lines[3] = "Line number 4"; lines[4] = "Line number 5"; lines[5] = "Line number 6"; Notification.InboxStyle inbox = new Notification.InboxStyle(); for(int i = 0 ; i<lines.length; i++) inbox.addLine(lines[i]); inbox.setBigContentTitle("Inbox in Expansion Mode"); mBuilder.setStyle(inbox); Notification notfi = mBuilder.build(); mNotificationManager.notify(97,notfi); } private void showBigPictureNotification() { mBuilder = new Notification.Builder(this); mBuilder.setSmallIcon(R.drawable.ic_picture); mBuilder.setContentTitle("Big Picture in Normal Mode"); mBuilder.setContentText("Example of a Big Picture Notification."); Bitmap img = BitmapFactory.decodeResource(getResources(),R.drawable.picture); Notification.BigPictureStyle bigPic = new Notification.BigPictureStyle(); bigPic.bigPicture(img); bigPic.setBigContentTitle("Big Picture in Expansion Mode"); mBuilder.setStyle(bigPic); Notification notfi = mBuilder.build(); mNotificationManager.notify(6,notfi); } } وملف واجهة المستخدم activity_main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Big Text Style" android:id="@+id/bg_txt"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Inbox Style" android:id="@+id/inbx"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Big Picture Style" android:id="@+id/bg_pic"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Cancel ALL" android:id="@+id/cncl"/> </LinearLayout> بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  10. فيما سبق من دروس هذه السلسلة، تعلمنا أساسيات لغات البرمجة وجافا خاصة، هناك بعض الأمور الهامة في لغة جافا والتي يتم استخدامها بكثرة في تطبيقات الأندرويد، وسنكمل في هذا الدرس ما بدأناه من أساسيات لغة Java. Method Overriding لشرح هذا المفهوم دعنا نوضح هذا المثال والذي يقوم بتعريف صنف جديد يدعى Shape وبداخله ثلاث توابع كما يلي: class Shape{ protected int width; protected int height; public void setWidth(int a){ width = a; } public void setHeight(int b){ height = b; } public int getArea(){ return 0; } } وإذا قمنا بعمل كائن جديد يمكننا من خلاله استدعاء التوابع الخاصة به. Shape sh = new Shape(); sh.setWidth(10); sh.setHeight(5); int area = sh.getArea(); في المثال السابق سيتم تخزين 0 في المتغير area وذلك ما يفعله التابع ()getArea، حيث يقوم دائمًا بإعادة القيمة 0 أيًا كانت قيمة الطول والعرض وذلك لأننا نعتبر هذا الصنف نوعًا عام غير محدد الشكل ولا يمكننا معرفة مساحته. سنقوم الآن بصنع صنف جديد يرث من Shape: class Square extends Shape{ } كما ذكرنا سابقًا سيرث منه المتغيرات والخصائص كما سيرث منه التوابع الخاصة به فلا داعي لتعريفها بداخله مرة أخرى. Square sq = new Square(); sq.setWidth(10); sq.setHeight(10); int area = sq.getArea(); ستظل في هذه الحالة قيمة المتغير area كما هي تساوي 0 حيث ورث الصنف Square التابع ()getArea كما هو دون أي تغيير. إذا أردنا تغيير القيمة التي يعيدها هذا التابع نستخدم مفهوم Method Overriding، وهو ببساطة يعني تجاوز المحتوى السابق لهذا التابع والذي تم استخدامه داخل الأب لهذا الصنف واستخدام محتوى جديد بدلًا منه عند استدعاء التابع. ولتطبيق هذا المفهوم نقوم بكتابة التابع والحفاظ على اسمه ونوع البيانات التي يُعيدها وتغيير المحتوى الداخلي له لتنفيذ الوظيفة الجديدة. class Square extends Shape{ public int getArea(){ return width * height; } } والآن عند كتابة الكائن السابق سنجد أن المتغير area تغيرت قيمته ليقوم بتخزين حاصل ضرب الطول والعرض وحساب المساحة، ولن يتغير المحتوى الخاص بالتابع الأصلي ()getArea المتواجد داخل الصنف Shape. Square sq = new Square(); sq.setWidth(10); sq.setHeight(10); int area = sq.getArea(); //area = 10 والميزة الرئيسية لهذا المفهوم أنه يجعل للصنف الذي يرث من صنف آخر القدرة على صنع خطواته الخاصة لتنفيذ أحد التوابع التي يرثها دون التغيير في التابع الأصلي للأب. قواعد تطبيق مفهوم Method Overriding يجب كتابة التابع دون تغيير في الاسم الخاص به أو تغيير نوع البيانات التي تُمرر له أو تغيير ترتيبها الأصلي كما لا يمكن تغيير نوع البيانات التي يُعيدها. غير مسموح بتقليل القيود المتواجدة في التابع والتي يتم تحديدها عن طريق Access Modifiers. فمثلًا إذا تم تعريف التابع على أنه public فلا يمكن تقييده وجعله protected أو private لأن ذلك أقل في صلاحيات الوصول لهذا التابع، أما إذا كان التابع الأصلي protected وتم تغييرها إلى public فهذا مسموح به لأنه أعطى صلاحية وصول أكبر للتابع. لا يمكن تجاوز التابع المُعرف على أنه final. ولاحظ أن final عند تعريف المتغيرات تعني ثابت لا يمكن تغيير قيمته، وعند تعريف التوابع تعني تابع لا يمكن تجاوزه وتغيير محتواه. إذا أردت تنفيذ محتوى التابع الأصلي لتابع تم تجاوزه يمكنك ذلك عن طريق استخدام super، وبتطبيق ذلك على المثال السابق: class Square extends Shape{ public int getArea(){ int a = super.getArea(); if ( a == 0 ){ return width * height; } else { return a; } } } الحواشي Annotations هي طريقة لتقديم معلومات معينة عن الشيفرة المكتوبة والتي تشير إليها هذه الملاحظة ولا يتم اعتبارها أنها جزء من الشيفرة ولا تؤثر بشكل مباشرة فيها. هناك استخدامات مختلفة للحواشي annotations منها إعطاء أوامر للمترجم الخاص بالبرنامج، ولهذا الاستخدام يوجد عدة حواشي مبنية داخل جافا وهي: Override@ ويتم استخدامها عند تجاوز تابع وذلك لجعل المترجم يقوم بالتأكد أننا نقوم حقًا بتجاوز تابع متواجد داخل الأب ونكتبه بشكل صحيح غير مخالف للقواعد. class Square extends Shape{ @Override public int getArea(){ return width * height; } } Deprecated@ ويتم استخدامها لإشارة إلى أن هذا التابع أو الصنف تم إزالته من اللغة وأصبح قديمًا لذا لا يجوز استعماله مرة أخرى ويجب استبداله، وعند كتابة هذه الملاحظة يقوم المترجم بكتابة تحذير للمطور بذلك حتى يُذكّره. وتعتبر الحواشي من الأشياء المهمة عند كتابة الشيفرات ومن الجيد التعود على استخدامها. Method Overloading هي طريقة للسماح للصنف بتعريف تابع أو أكثر لها نفس الاسم. وهناك بعض الشروط الواجب توافرها لتطبيق هذا المفهوم بشكل صحيح فيجب أن تختلف التوابع ذات الاسم نفسه في إحدى هذه العناصر عدد المعاملات Parameters التي يتمر تمريرها. نوع المعاملات التي يتم تمريرها. ترتيب المُعاملات التي يتم تمريرها. class Exampe{ public int add(int x,int y){ return x+y; } public int add(int x,int y,int z){ return x+y+z; } public float add(float x,float y){ return x+y; } } في المثال السابق استخدمنا نفس التابع ()add ولكن بأشكال مختلفة وقمنا بتطبيق إحدى القواعد المطلوبة في كل تابع لتحقيق شرط هذا المفهوم. ولاحظ أنه لا يمكننا كتابة هذا الشكل: public int add(int y,int x){ return x+y; } حيث أنه لا يوجد فرق بينه وبين الشكل الأول فلا يمكننا التمييز بتغيير اسم المتغيرات. ArrayList يوجد داخل لغة جافا صنف يدعى ArrayList وهو يعبر عن قائمة من البيانات تتميز بالمرونة والقدرة على القيام بوظائف عديدة، ويتم تفضيلها عادة على استخدام مصفوفة البيانات التقليدية وذلك لأن المصفوفة يتم تحديدها بعدد من البيانات لا يمكن تغييره، فلا يمكن إضافة عناصر جديدة لها كما لا يمكن إزالة عناصر منها وتقليل العدد. وهذا ما يميز ArrayList لقدرتها على تغيير حجمها والتكييف حسب البيانات المخزنة بداخلها. كما تتميز ArrayList عن Array أو المصفوفة التقليدية بوجود توابع مختلفة تقوم بوظائف عديدة على عكس Array الذي يملك توابع. لتعريف كائن جديد من الصنف ArrayList: ArrayList<String> strObject = new ArrayList<String>(); في المثال السابق قمنا بتعريف كائن من الصنف ArrayList يدعى strObject ويستطيع تخزين بداخله بيانات من النوع String. الآن بعد أن قمنا بتعريف الكائن هناك عدة توابع يمكننا استخدامها مثل: (add(o وهو يقوم بإضافة العنصر (o) إلى القائمة. strObject.add(“Ahmed”); // [“Ahmed”] strObject(“Mohamed”); // [“Ahmed”,”Mohamed”] strObject(“Mariam”); // [“Ahmed”,”Mohamed”,”Mariam”] في المثال السابق نضيف عناصر من النوع String إلى القائمة باستدعاء التابع ()add وتمرير له العنصر الذي نريد إضافته، وفي كل مرة يتم استدعاء التابع يتم تغيير حجم القائمة بشكل مرن. (add(I ,o يختلف عن التابع السابق بأنه يقوم بتحديد المكان (I) الذي يرغب بتخزين العنصر فيه، ففي التابع السابق يتم إضافة العناصر في ذيل القائمة. strObject.add(2,“Sara”); // [“Ahmed”,”Mohamed”,”Sara”,”Mariam”] (set(I,O تقوم بتبديل العنصر المتواجد في المكان (I) بالعنصر (O). strObject.set(1,“Tarek”); // [“Ahmed”,”Tarek”,”Sara”,”Mariam”] في المثال السابق سيتم استبدال العنصر "Mohamed" بالعنصر "Tarek". لاحظ أنه يبدأ الترقيم الخاص بالعناصر من صفر. (get(I يُعيد هذا التابع العنصر المخزن في المكان (I). String name = strObject.get(0); //nama = “Ahmed” في المثال السابق نحصل على العنصر "Ahmed" والمتواجد في المكان 0 (رأس القائمة). ()size لمعرفة عدد العناصر المخزنة داخل القائمة. int numberOfElements = strObject.size(); // numberOfElements = 4 (remove(O لإزالة عنصر محدد من القائمة. strObject.remove(“Mariam”); // [“Ahmed”,”Tarek”,”Sara”] (remove(I لإزالة عنصر المتواجد في المكان (I). strObject.remove(1); // [“Ahmed”,”Sara”] ()clear لإزالة كافة عناصر القائمة. strObject.clear(); // [] وكما قمنا باستخدام ArrayList مع النصوص يمكننا استخدامها مع أي صنف أخر فمثلا يمكنا عمل قائمة من المربعات (صنف Square)، وهو الصنف الذي قمنا بصناعته في أول الدرس. ArrayList<Square> obj = new ArrayList<Square>(); ولإضافة مربع جديد للقائمة. Obj.add(new Square()); ولتغيير الطول والعرض الخاصين بهذا المربع. Obj.get(0).setWidth(15); Obj.get(0).setHeight(15); وهكذا يمكننا التعامل مع عناصر القائمة بنفس الطريقة، فكل عنصر داخل القائمة هو كائن من Square. لاحظ أنه هناك توابع خاصة بالصنف ArrayList وأخرى خاصة بالصنف Square. Interface تتشابه الواجهات (Interface) في بينتها مع الأصناف (Class)، فيمكننا بداخله تعريف توابع ومتغيرات لكنها ذات طبيعة خاصة. فجميع التوابع داخل الواجهة تتكون من تعريف فقط ولا يوجد لها محتوى، وعلى الصنف الذي يُنفذ الواجهة أن يكتب المحتوى الخاص بالتابع. public interface MyInterface { public int method1(); public void method2(); } ولا يمكن إنشاء كائنات من الواجهة (Interface). ولتنفيذ الواجهة نستخدم implements، ويمكن للأصناف (Classes) فقط أن تُنفذ الواجهات (Interfaces). وبداخل الصنف يجب كتابة المحتوى الخاص بالتوابع التي تم تعريفها داخل الواجهة، كما يمكننا أن نكتب التوابع الخاصة بالصنف كما سبق. public class X implements MyInterface{ public int method1(){ return 0; } public void method2(){ } } بعد ذلك يمكننا إنشاء كائنات من الصنف X واستدعاء التوابع كما فعلنا سابقًا. X obj = new X(); Obj.method2(); واستخدام الواجهة هو الطريقة المثالية لتزويد المطور بالتوابع اللازم كتابتها لأداء مهمة ما، فهي تضمن أن الصنف الذي يُنفذ الواجهة قد قام بكتابة كافة التوابع الخاصة به.
  11. لدى برمجة تطبيقات أندرويد، فإنّه وفي العديد من الأحيان نحتاج أن نعرض مجموعة من العناصر معًا في النشاط أمام المستخدم كعرض جهات الاتصال مثلًا أو الرسائل حيث يمكن للمستخدم أن يتصفحها ويتنقل بينها سريعًا، كما يمكنه الضغط على أي منها فيتم عرض المزيد من المعلومات عن هذا العنصر. نستخدم عنصر الواجهة ListView أو قائمة العرض للقيام بما سبق والذي يتيح عرض أكثر من عنصر في قائمة واحدة، ويتم إضافة العناصر إلى قائمة العرض تلقائيًا باستخدام كائن من الصنف Adapter والذي يستطيع جلب المحتوى من مصفوفة من البيانات أو قاعدة بيانات. ويُعتبر الصنف Adapter بمثابة حلقة الوصل بين عنصر الواجهة الخاص بقائمة العرض ومصدر البيانات المستخدم حيث يقوم بتشكيلها بالشكل المطلوب لتُمثل عنصرًا من عناصر القائمة. يوجد عدة أصناف فرعية من الصنف Adapter كي تُستخدم مع أشكال البيانات المختلفة مثل: ArrayAdapter Base Adapter SimpleCursorAdapter قوائم العرض مع كائن من ArrayAdapter تطبيق 1 يُستخدم ArrayAdapter عندما يكون المصدر البيانات هو قائمة أو مصفوفة. سنقوم بصنع تطبيق يعرض مجموعة من النصوص في قائمة العرض، لذا سنقوم بعمل مشروع جديد في Android Studio. ولتكوين قائمة العرض نقوم بالخطوات التالية: نضع عنصر من ListView في ملفات الواجهة ونشير له بـ Id مميز، ثم نربط هذا العنصر بشيفرات الجافا باستخدام التابع ()findViewById. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/listview"/> </LinearLayout> نصنع ملف واجهة جديد يكون الجذر له عنصر من النوع TextView. ولصنع واجهة استخدام جديدة نضغط على المجلد layout المتواجد داخل res بالزر الأيمن ثم نختار: new > XML > Layout XML File ونسميه row_item. وبداخل هذا الملف نجعل عنصر الجذر من النوع TextView ونغير حجم النص الذي سيُكتب بداخله إلى 20sp ونجعل الخاصية padding لها قيمة 20sp. لاحظ أن الخاصية padding تعني الحشو أي ضع فراغًا مقداره 20sp حول النص المكتوب داخل TextView في كل الاتجاهات. <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="20sp" android:padding="20sp"/> في شيفرة الجافا نبني كائنًا جديدًا من الصنف ArrayAdapter ونمرر لدالة البناء الخاصة به ثلاث عناصر: العنصر الأول هو this والذي يعبر عن السياق الخاص بالنشاط المكوّن للقائمة. العنصر الثاني هو TextView الذي سيُعرض فيه كل نص في القائمة. العنصر الأخير هو المصفوفة الخاصة بالنصوص التي سيتم عرضها في قائمة العرض. ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.row_item,colorsArray); نستدعي التابع ()setAdapter باستخدام الكائن الخاص بالـ ListView ونمرر له الـ Adapter الذي قمنا بصنعه ليصبح التطبيق النهائي: package apps.noby.listviewexample; import android.app.Activity; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; public class MainActivity extends Activity { private ListView lv; String [] colorsArray = {"Red","Yellow","Blue","Orange","Black","Green","Brown","Grey"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv =(ListView) findViewById(R.id.listview); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.row_item,colorsArray); lv.setAdapter(adapter); } } ثم نقوم بتجربة التطبيق على المحاكي لنتأكد من عمله بالشكل المطلوب. تطبيق 2 في هذا التطبيق بدلًا من استخدام المصفوفة لتخزين البيانات سنقوم باستبدالها بقائمة من النوع ArrayList مع الحفاظ على باقي ملفات الواجهة السابقة لتصبح الشفرة النهائية بعد التعديل: package apps.noby.listviewexample; import android.app.Activity; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; import java.util.ArrayList; public class MainActivity extends Activity { private ListView lv; ArrayList<String> colorsList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); colorsList = new ArrayList<String>(); colorsList.add("Red"); colorsList.add("Yellow"); colorsList.add("Blue"); colorsList.add("Orange"); colorsList.add("Black"); colorsList.add("Green"); colorsList.add("Brown"); colorsList.add("Grey"); lv =(ListView) findViewById(R.id.listview); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.row_item,colorsList); lv.setAdapter(adapter); } } وبتجربة هذا التطبيق على المحاكي نجد أنه يقوم بنفس الوظيفة السابقة، ولكن استخدامنا للقائمة بدلًا من المصفوفة يتيح لنا القدرة على إضافة أو إزالة العناصر بسهولة وهو ما لا تسمح به المصفوفة. استجابة عناصر القائمة عند الضغط عليها تطبيق 3 سنقوم في هذا التطبيق بجعل العناصر المتواجدة بقائمة العرض تستجيب عند الضغط عليها، فكما ذكرنا سابقًا أننا استخدمنا كائنًا من نوع ArrayList لسهولة إضافة أو إزالة العناصر منه، سنقوم الآن بتطبيق هذا المفهوم حيث سنقوم بإزالة العنصر من القائمة عند الضغط عليه. كما نستخدم التابع ()setOnClickListener مع الزر للاستجابة عند الضغط عليه سنقوم هنا باستخدام التابع ()setOnItemClickListener للاستجابة عند الضغط على عناصر القائمة، وبداخله نجد الدالة ()onItemClick وبها العناصر التالية: parent وهو يُعبر عن الأب الخاص بالعنصر الذي تم الضغط عليه وفي هذا المثال فهو ListView. view ويُمثل العنصر نفسه الذي تم الضغط عليه أي TextView. position ويُعبر عن رقم العنصر في القائمة. Id وهو رقم مميز يتم تحديده لعنصر الواجهة الذي يعرض النص. لتصبح الشفرة النهائية الخاصة بهذا التطبيق: package apps.noby.listviewexample; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import java.util.ArrayList; public class MainActivity extends Activity { private ListView lv; ArrayList<String> colorsList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); colorsList = new ArrayList<String>(); colorsList.add("Red"); colorsList.add("Yellow"); colorsList.add("Blue"); colorsList.add("Orange"); colorsList.add("Black"); colorsList.add("Green"); colorsList.add("Brown"); colorsList.add("Grey"); lv =(ListView) findViewById(R.id.listview); final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.row_item,colorsList); lv.setAdapter(adapter); lv.setOnItemClickListener(new ListView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { String item = (String) parent.getItemAtPosition(position); colorsList.remove(item); adapter.notifyDataSetChanged(); } }); } } بداخل التابع ()onItemClick نستخدم الكائن parent لاستدعاء التابع ()getItemAtPosition وتمرير رقم العنصر له حتى يُعيد لنا النص المخزن في ذلك العنصر. ثم بعد ذلك نقوم بإزالة العنصر من القائمة واستدعاء التابع ()notifyDataSetChanged باستخدام الكائن adapter وذلك لتنفيذ التغيير الذي حدث على عناصر قائمة العرض. الآن نقوم بتجربة التطبيق على المحاكي، نجد أنه عند الضغط على أحد العناصر تختفي في الحال. يمكنك تطوير هذا التطبيق لجعله يستدعي نشاطًا جديدًا عند الضغط على أحد العناصر. البحث داخل عناصر قائمة العرض تطبيق 4 في هذا التطبيق سنقوم بالبحث عن العناصر التي تحتوي على نص معين وعرضها هي فقط وذلك لسهولة الوصول لعنصر محدد. كتطوير على التطبيق السابق سنقوم بإضافة الخاصية Orientation للعنصر LinearLayout في ملف activity_main.xml. android:orientation="vertical" ثم نضيف عنصر EditText قبل ListView وهو العنصر الذي سيقوم المستخدم بالتفاعل معه وكتابة النص الذي سيبحث عنه داخل قائمة العرض. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:drawableLeft="@drawable/ic_search_black_24dp" android:hint="Search..." android:id="@+id/searchedittext"/> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/listview"/> </LinearLayout> ولإضافة صورة يسار العنصر EditText نستخدم الخاصية drawableLeft ثم نضيف الصورة من المجلد drawable. الآن سنربط هذا العنصر بالشيفرة الرئيسية للتطبيق ثم نستدعي التابع ()addTextChangedListener والذي يستجيب لأي تغير يحدث داخل EditText، حيث يوفر ثلاث دوال: searchET.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { adapter.getFilter().filter(s); } @Override public void afterTextChanged(Editable s) { } }); كل دالة تختص بفترة محددة، وهنا نهتم بالدالة ()onTextChanged والتي يتم استدعاؤها لحظة تغير النص المكتوب بداخل EditText. أخيرًا لتنقية القائمة وترك فقط العناصر التي يتم البحث عنها نستدعي التابع ()getFilter باستخدام الكائن الخاص بالـ ArrayAdapter ثم نستدعي بعده مباشرة التابع ()filters ونمرر له المتغير s الذي يحمل بداخله النص الذي قمنا بكتابته في EditText. لتصبح الشيفرة النهائية كما يلي: package apps.noby.listviewexample; import android.app.Activity; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; import java.util.ArrayList; public class MainActivity extends Activity { private ListView lv; private ArrayList<String> colorsList; EditText searchET; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); colorsList = new ArrayList<String>(); colorsList.add("Red"); colorsList.add("Yellow"); colorsList.add("Blue"); colorsList.add("Orange"); colorsList.add("Black"); colorsList.add("Green"); colorsList.add("Brown"); colorsList.add("Grey"); searchET = (EditText) findViewById(R.id.searchedittext); lv =(ListView) findViewById(R.id.listview); final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.row_item,colorsList); lv.setAdapter(adapter); lv.setOnItemClickListener(new ListView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { String item = (String) parent.getItemAtPosition(position); colorsList.remove(item); adapter.notifyDataSetChanged(); } }); searchET.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { adapter.getFilter().filter(s); } @Override public void afterTextChanged(Editable s) { } }); } } بعد ذلك نقوم بتجربة التطبيق على المحاكي لنتأكد من عمله كما ينبغي. تخصيص واجهة قائمة العرض تطبيق 5 تُتيح قائمة العرض ميزة تخصيص العناصر، فيمكن صنع قائمة عرض تتكون من نص وصورة في كل صف بدلًا من نص فقط كالأمثلة السابقة. في هذا التطبيق سنقوم بصنع تطبيق يعرض قائمة كما في الصورة التالية، وعند الضغط على العنصر يعرض نشاطًا جديدًا. من Android Studio نقوم بعمل مشروع جديد، نبدأ أولًا بصنع واجهة المستخدم الرئيسية والمتكونة من ListView. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/listview"/> </LinearLayout> نصنع ملف واجهة جديد يُعبر عن الصف ويدعى row_item وبداخله نكون الشكل المطلوب للصف في قائمة العرض كما في المخطط التالي: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/blue" android:id="@+id/imgview"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Title Example" android:textSize="20sp" android:textStyle="bold" android:id="@+id/titleview"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Description Example" android:textSize="15sp" android:id="@+id/descview"/> </LinearLayout> </LinearLayout> يتكون الصف في قائمة العرض من صورة، نص العنوان ونص الوصف لذا سنصنع صنفًا جديدًا يحتوي بداخله على هذه المعلومات الخاصة بالعنصر ثم نكوّن مصفوفة أو قائمة من هذه الصنف. لصنع صنفًا جديدًا من داخل المجلد java اضغط بالزر الأيمن على اسم الحزمة الخاصة بالمشروع واختر New > Java Class ثم اكتب اسم الصنف الجديد ListItem. بداخل هذا الصنف الجديد نعرّف ثلاث متغيرات، package apps.noby.customlistviewexample; public class ListItem { public int imgSrc; public String title; public String desc; } في ملف MainActivity.java نكتب الشيفرة الخاصة بالمشروع. أولًا نصنع كائنًا من الصنف ArrayList ليكون هو مصدر البيانات التي ستُعرض في قائمة العرض، ويتكون الكائن من مجموعة من الكائنات الأخرى من الصنف ListItem لكل منها صورة خاصة به، نص العنوان ونص الوصف. private ArrayList<ListItem> items; String [] colorsArray = {"Red","Yellow","Blue","Black","Green","Brown","Grey"}; int [] colorsImage = {R.drawable.red,R.drawable.yellow,R.drawable.blue,R.drawable.black,R.drawable.green,R.drawable.brown,R.drawable.grey}; items = new ArrayList<ListItem>(); for(int i=0;i<colorsArray.length;i++){ ListItem item= new ListItem(); item.imgSrc=colorsImage; item.title = colorsArray; item.desc = colorsArray + " Color!"; items.add(item); } ثانيًا بعد ذلك نصنع الكائن الخاص بالـ ArrayAdapter ولكننا لا يمكننا استخدام الصنف الأساسي من ArrayAdapter حيث أنه لا يعمل إلا مع نص واحد فقط ولن يمكننا تغيير الصورة أو النص الثاني، لذا ينبغي علينا صنع الصنف ArrayAdapter الخاص بنا. بنفس الطريقة السابقة نصنع صنف جديد من داخل المجلد Java، اضغط بالزر الأيمن على اسم الحزمة الخاصة بالمشروع واختر New > Java Class ثم اكتب اسم الصنف الجديد CustomArrayAdapter. يجب أن يرث هذا الصنف الفرعي من الصنف الأساسي ArrayAdapter. public class CustomArrayAdapter extends ArrayAdapter ثم نقوم بكتابة دالة البناء الخاصة بهذا الصنف ونستدعى في بدايتها دالة البناء الخاصة بالصنف الأب، وتحتاج دالة البناء لكائن من الصنف Context وهو السياق الذي سيعمل فيه هذا الـ Adapter أي النشاط الذي سيعمل به، كما يحتاج إلى واجهة المستخدم التي تكون شكل الصف، وقائمة البيانات التي سيتم عرضها. private Context con; private ArrayList<ListItem> data; private int resLayout; public CustomArrayAdapter(Context context, int resource, List objects) { super(context, resource,objects); con = context; resLayout = resource; data =(ArrayList) objects; } يوجد تابع داخل ArrayAdapter يُدعى ()getCount ويتم استخدامه بشكل تلقائي عندما يريد الكائن الذي سنصنعه من CustomArrayAdapter معرفة عدد العناصر التي سيقوم بعرضها داخل قائمة العرض، لذا فهو يستدعي التابع ()size باستخدام الكائن الخاص بالبيانات. @Override public int getCount() { return data.size(); } وبداخل الصنف الأب ArrayAdapter يوجد أيضًا تابع يُدعى ()getView، وهو المسؤول عن تكوين الشكل الخاص بالصف المعروض في قائمة العرض لذا كي نصنع الصف الخاص بنا يجب علينا تعديل هذا التابع. ولتعديل التابع نقوم بكتابته بطريقتنا الخاصة كما يلي: @Override public View getView(int position, View convertView, ViewGroup parent) { inflater = (LayoutInflater) con.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View rootView = inflater.inflate(resLayout,null); ImageView img = (ImageView) rootView.findViewById(R.id.imgview); TextView title = (TextView) rootView.findViewById(R.id.titleview); TextView desc = (TextView) rootView.findViewById(R.id.descview); ListItem item = data.get(position); img.setImageResource(item.imgSrc); title.setText(item.title); desc.setText(item.desc); return rootView; } داخل التابع نبدأ أولًا بطلب الكائن LayoutInflater وهو المسؤول عن تحويل ملف XML إلى كائن من النوع View وللحصول عليه نستخدم التابع ()getSystemService والذي يوفر مجموعة مختلفة من الخدمات منها الكائن LayoutInflater، ويتم تحديد نوع الخدمة المطلوبة عن طريق تمرير ثابت محدد وفي هذه الحالة هو Context.LAYOUT_INFLATER_SERVICE. بعد الحصول على هذا الكائن نستدعي التابع ()inflate ونمرر له ملف XML الذي نرغب في تحويله إلى كائن في ملف الجافا. inflater = (LayoutInflater) con.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View rootView = inflater.inflate(resLayout,null); الآن لربط عناصر الواجهة بالشيفرة نستخدم التابع ()findViewById ولكنا هنا لا نستطيع استخدامه مباشرة كما كنا نفعل عند وجودنا داخل شيفرة النشاط، لذا يجب أن نستدعي التابع باستخدام الكائن من الصنف View الذي حصلنا عليه من التابع ()inflate. ImageView img = (ImageView) rootView.findViewById(R.id.imgview); TextView title = (TextView) rootView.findViewById(R.id.titleview); TextView desc = (TextView) rootView.findViewById(R.id.descview); أخيرًا نأتي بالبيانات المناسبة للعنصر من داخل الـ ArrayList ثم نضع بداخل كل عنصر من عناصر الواجهة البيانات الخاصة به. ListItem item = data.get(position); img.setImageResource(item.imgSrc); title.setText(item.title); desc.setText(item.desc); لتصبح الشيفرة النهائية داخل الصنف CustomArrayAdapter: package apps.noby.customlistviewexample; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; import java.util.List; public class CustomArrayAdapter extends ArrayAdapter { private Context con; private ArrayList<ListItem> data; private LayoutInflater inflater; private int resLayout; public CustomArrayAdapter(Context context, int resource, List objects) { super(context, resource,objects); con = context; data =(ArrayList) objects; resLayout = resource; } @Override public int getCount() { return data.size(); } @Override public View getView(int position, View convertView, ViewGroup parent) { inflater = (LayoutInflater) con.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View rootView = inflater.inflate(resLayout,null); ImageView img = (ImageView) rootView.findViewById(R.id.imgview); TextView title = (TextView) rootView.findViewById(R.id.titleview); TextView desc = (TextView) rootView.findViewById(R.id.descview); ListItem item = data.get(position); img.setImageResource(item.imgSrc); title.setText(item.title); desc.setText(item.desc); return rootView; } } نعود مجددًا إلى ملف MainActivity.java لإنشاء كائن من الصنف الذي صنعناه: private CustomArrayAdapter adapter; adapter = new CustomArrayAdapter(this,R.layout.row_item,items); ثالثًا نربط قائمة العرض بالشيفرة ونستدعي التابع ()setAdapter: private ListView customLV; customLV = (ListView) findViewById(R.id.listview); customLV.setAdapter(adapter); بهذا نكون قد انتهينا من تكوين قائمة العرض المطلوبة، يتبقى أن نجعل عناصر قائمة العرض تستجيب عند الضغط عليها وتقوم بفتح نشاط جديد، لذا أولًا نقوم بعمل نشاط جديد بالضغط بالزر اليمن على اسم الحزمة واختيار: New > Activity > Empty Activity ونسميها DescriptionActivity. ولفتح هذا النشاط عند الضغط على عناصر قائمة العرض نقوم باستدعاء التابع ()setOnItemClickListener وبداخله نفتح النشاط الجديد ونرسل إليه اسم العنصر الذي قام بفتحه حتى يعرضه. package apps.noby.customlistviewexample; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import java.util.ArrayList; public class MainActivity extends Activity { private ListView customLV; private CustomArrayAdapter adapter; private ArrayList<ListItem> items; String [] colorsArray = {"Red","Yellow","Blue","Black","Green","Brown","Grey"}; int [] colorsImage = {R.drawable.red,R.drawable.yellow,R.drawable.blue,R.drawable.black,R.drawable.green,R.drawable.brown,R.drawable.grey}; public static final String COLOR_KEY = "colorName"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); items = new ArrayList<ListItem>(); for(int i=0;i<colorsArray.length;i++){ ListItem item= new ListItem(); item.imgSrc=colorsImage; item.title = colorsArray; item.desc = colorsArray + " Color!"; items.add(item); } customLV = (ListView) findViewById(R.id.listview); adapter = new CustomArrayAdapter(this,R.layout.row_item,items); customLV.setAdapter(adapter); customLV.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent intent = new Intent(MainActivity.this,DescriptionActivity.class); intent.putExtra(COLOR_KEY,colorsArray[position]); startActivity(intent); } }); } } أخيرًا نقوم بتهيئة واجهة المستخدم الخاصة بالنشاط الجديد لتعرض النص المرسل إليها. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/txt" android:textSize="20sp"/> </LinearLayout> ولعرض النص داخل الـ TextView نقوم في الشيفرة باستدعاء الـ Intent الذي قام بفتح النشاط واستخراج منه البيانات المرسلة وكتابتها على TextView كما يلي: package apps.noby.customlistviewexample; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class DescriptionActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_description); String str = getIntent().getStringExtra(MainActivity.COLOR_KEY); TextView tv = (TextView) findViewById(R.id.txt); tv.setText("Welcome to " + str + " Color Home!!"); } } والآن نقوم بتجربة التطبيق على المحاكي حتى نتأكد من عمله بالشكل المطلوب. البحث داخل عناصر قائمة العرض التي تم تخصيصها تطبيق 6 في هذا التطبيق سنكرر ما قمنا به سابقًا من بحث عن نص معين داخل قائمة العرض ولكن الاختلاف هنا سيكون أن قائمة العرض ليست بهذه البساطة وتتكون من عنصر واحد فقط وهو النص، ولكنها تتكون من صور وعدة نصوص مختلفة لذا ينبغي علينا أن نصنع بأنفسنا طريقتنا الخاصة للبحث وتنقية العناصر من القائمة وترك العناصر التي يتم البحث عنها. أولًا نقوم بتغيير ملف الواجهة activity_main.xml وإضافة عنصر EditText. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:drawableLeft="@drawable/ic_search_black_24dp" android:hint="Search..." android:id="@+id/searchedittext"/> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/listview"/> </LinearLayout> ثانيًا نربط عنصر EditText في الشيفرة الأساسية ونستدعي التابع ()addTextChangedListener وبداخل الدالة ()onTextChanged نستدعي التابع الخاص بالتنقية كما سبق. package apps.noby.customlistviewexample; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.View; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ListView; import java.util.ArrayList; public class MainActivity extends Activity { private ListView customLV; private CustomArrayAdapter adapter; private ArrayList<ListItem> items; String[] colorsArray = {"Red", "Yellow", "Blue", "Black", "Green", "Brown", "Grey"}; int[] colorsImage = {R.drawable.red, R.drawable.yellow, R.drawable.blue, R.drawable.black, R.drawable.green, R.drawable.brown, R.drawable.grey}; public static final String COLOR_KEY = "colorName"; EditText searchET; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); items = new ArrayList<ListItem>(); for (int i = 0; i < colorsArray.length; i++) { ListItem item = new ListItem(); item.imgSrc = colorsImage; item.title = colorsArray; item.desc = colorsArray + " Color!"; items.add(item); } searchET = (EditText) findViewById(R.id.searchedittext); customLV = (ListView) findViewById(R.id.listview); adapter = new CustomArrayAdapter(this, R.layout.row_item, items); customLV.setAdapter(adapter); customLV.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent intent = new Intent(MainActivity.this, DescriptionActivity.class); intent.putExtra(COLOR_KEY, colorsArray[position]); startActivity(intent); } }); searchET.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { adapter.getFilter().filter(s); } @Override public void afterTextChanged(Editable s) { } }); } } أخيرًا يتبقى أن نقوم بكتابة تابع التنقية الخاص بنا، وسنقوم بكتابته داخل الصنف CustomArrayAdapter. داخل CustomArrayAdapter نقوم بإنشاء كائن آخر من ArrayList ونجعله يساوي البيانات التي سنضعها في قائمة العرض، مثله كالكائن data. private ArrayList<ListItem> dataTemp; public CustomArrayAdapter(Context context, int resource, ArrayList<ListItem> objects) { super(context, resource,objects); con = context; data = objects; resLayout = resource; dataTemp = objects; } بعد ذلك نعيد كتابة التابع الخاص بالتنقية والمتواجد أيضًا داخل الأب Arrayadapter لذا سنقوم بعمل Override لهذا التابع. والتابع هو ()getFilter ويعيد كائن من الصنف Filter وبداخل هذا الصنف تظهر لدينا دالتين جديدتين كما يلي: @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { } @Override protected void publishResults(CharSequence constraint, FilterResults results) { } }; } الأولى ()performFiltering هي المسؤولة عن عملية التنقية ونكتب بداخلها كيف تتم تنقية العناصر وما هي العناصر التي ستظهر عند كتابة نص معين ويتم تخزين النص الذي تم كتابته وعلى أساسه تتم عملية التنقية داخل المتغير constraint. الثانية ()publishResults هي المسؤولة عن تغيير محتوي البيانات التي تعرضها قائمة العرض حيث تأتي النتائج بعد التنقية داخل الكائن results. سنبدأ أولًا بكتابة طريقة التنقية التي سنتبعها وهي، نتأكد أن النص الذي ستتم التنقية على أساسه وممثل في المتغير constraint ليس فارغًا وذلك عن طريق الجملة المنطقية التالية: constraint == null || constraint.length() == 0 وتعني هل المتغير constraint لم يُخزن بداخله أي بيانات أو تم تخزين نص فارغ، لذا سنضع هذه الجملة داخل الجملة الشرطية if وإن تحققت فذلك يعني أنه لا داعي للتنقية وستظل عناصر القائمة كما هي، أما إذا كان خلاف ذلك ويوجد نص فسنقوم بعملية التنقية. FilterResults results = new FilterResults(); if(constraint == null || constraint.length() == 0){ results.values = dataTemp; results.count = dataTemp.size(); } else{ } return results; لاحظ أن الكائن results هو المتغير الذي سنخزن بداخله ناتج التنقية، ويتكون من عنصرين مهمين الأول results.values وهنا يتم تخزين البيانات الجديدة و results.count ويتم تخزين عددهم. طريقة التنقية ستكون كالتالي: نصنع حاوية جديدة لتخزين العناصر الناتجة من التنقية. لكل عنصر من العناصر القديمة في قائمة العرض نقوم بمقارنته بالنص المكتوب لنرى هل يحتوي على حروفه. إن كان يحتوي على أحد حروفه ننقله إلى حاوية التخزين الجديدة التي صنعناها. بعد الانتهاء من كافة العناصر نعيد النتيجة ليتم تحديث قائمة العرض بالعناصر الجديدة. ArrayList<ListItem> filterList = new ArrayList<ListItem>(); for (int i = 0; i < dataTemp.size(); i++) { ListItem item = dataTemp.get(i); if ( (item.title.toLowerCase()).contains(constraint.toString().toLowerCase())) { ListItem filterItem = new ListItem(); filterItem.imgSrc = dataTemp.get(i).imgSrc; filterItem.title = dataTemp.get(i).title; filterItem.desc = dataTemp.get(i).desc; filterList.add(filterItem); } } results.values = filterList; results.count = filterList.size(); لاحظ أننا نقوم بالتنقية على أساس النص المخزن داخل title، وأننا نقوم بتحويل هذا النص إلى حروف صغيرة ومقارنته بالنص الذي نبحث عنه أيضًا بعد تحويله لحروف صغيرة لتوحيد طريقة كتابة الكلمة. ونستخدم التابع ()contains والذي يتأكد هل الكائن الذي قام باستدعاء هذا التابع يحتوي على النص الذي نمرره للتابع أم لا، إن كان يحتويه فسيعيد القيمة المنطقية true أما إذا كان لا يحتويه فسيعيد false. بعد ذلك ننتقل إلى الدالة الأخرى لنشر النتائج الجديدة في قائمة العرض، ونقوم بنقل البيانات المخزنة في results.values إلى المتغير data حتى يتم تحديث قائمة العرض باستدعاء التابع ()getView بعد استدعاء ()notifyDataChanged. data = (ArrayList<ListItem>) results.values; notifyDataSetChanged(); لتصبح الشيفرة النهائية للصنف CustomArrayAdapter بعد التعديل: package apps.noby.customlistviewexample; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Filter; import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; public class CustomArrayAdapter extends ArrayAdapter{ private Context con; private ArrayList<ListItem> data; private ArrayList<ListItem> dataTemp; private LayoutInflater inflater; private int resLayout; public CustomArrayAdapter(Context context, int resource, ArrayList<ListItem> objects) { super(context, resource,objects); con = context; data = objects; resLayout = resource; dataTemp = objects; } @Override public int getCount() { return data.size(); } @Override public View getView(int position, View convertView, ViewGroup parent) { inflater = (LayoutInflater) con.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View rootView = inflater.inflate(resLayout,null); ImageView img = (ImageView) rootView.findViewById(R.id.imgview); TextView title = (TextView) rootView.findViewById(R.id.titleview); TextView desc = (TextView) rootView.findViewById(R.id.descview); ListItem item = data.get(position); img.setImageResource(item.imgSrc); title.setText(item.title); desc.setText(item.desc); return rootView; } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults results = new FilterResults(); if (constraint == null || constraint.length() == 0) { results.values = dataTemp; results.count = dataTemp.size(); } else{ ArrayList<ListItem> filterList = new ArrayList<ListItem>(); for (int i = 0; i < dataTemp.size(); i++) { ListItem item = dataTemp.get(i); if ( (item.title.toLowerCase()).contains(constraint.toString().toLowerCase())) { ListItem filterItem = new ListItem(); filterItem.imgSrc = dataTemp.get(i).imgSrc; filterItem.title = dataTemp.get(i).title; filterItem.desc = dataTemp.get(i).desc; filterList.add(filterItem); } } results.values = filterList; results.count = filterList.size(); } return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { data = (ArrayList<ListItem>) results.values; notifyDataSetChanged(); } }; } } الآن قم بتجربة التطبيق على المحاكي للتأكد انه يعمل كما ينبغي. بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  12. تنتشر الهواتف المحمولة والحواسيب اللوحية العاملة بنظام تشغيل أندرويد في أحجام مختلفة ودقة شاشات مختلفة، لذلك من الأفضل دائمًا إذا استطاع التطبيق أن يتأقلم مع حجم ودقة الشاشة لتقديم أفضل تجربة استخدام مع الاستغلال الأمثل لشاشة. ويمكن أن تبني تطبيقًا تتغير الواجهة الخاصة به بشكل ديناميكي تبعًا للمساحة المتاحة في الشاشة، فإذا لم تتواجد مساحة كافية، يظهر جزء من الواجهة فقط في الشاشة ومنه يمكن الانتقال للواجهة الأخرى مثل الصورة التالية: أما إذا توافرت المساحة الكافية فيمكن دمج أكثر من واجهة في واجهة واحدة لسهولة التنقل بينها كما في الصورة التالية: لذا يوفر أندرويد عدة طرق للقيام بذلك من ضمنها استخدام الـ Fragment داخل التطبيق. Fragment يُعبر مصطلح Fragment عن تجزئة واجهة المستخدم إلى وحدات أصغر تُمكن المطوّر من دمج أو فصل هذه الوحدات تبعًا لحجم الشاشة، ولا تتكون تلك الوحدة من عناصر واجهة المستخدم فقط ولكنها أيضًا تتكون من شيفرات تتحكم في وظيفة هذه الواجهة وسلوكها. لكن هذه الوحدات ليست مستقلة بذاتها أي لا يمكن أن تُعرض بداخل التطبيق إلا من خلال نشاط “Activity” يستضيفها بداخله. ولهذا المفهوم العديد من المزايا التي توفرها عند تطوير التطبيقات ومنها: تقسيم الشيفرات المعقدة التي تتواجد في واجهة نشاط واحد إلى عدة وحدات لكل منها شيفرة بسيطة يتم دمجها وترتيبها بعد ذلك داخل واجهة واحدة. إمكانية إعادة استخدام الوحدة عدة مرات في أنشطة مختلفة ودمجها مع وحدات أخرى مختلفة. التكييف مع الواجهات المختلفة والأحجام المختلفة. دورة حياة التجزئة Fragment للـ Fragment دورة حياة مشابهة لدورة حياة النشاط ويتم استدعاء التوابع الخاصة بدورة الحياة عند حدوث حدث معين كإنشاء أو إزالة الـ Fragment. ()onAttach: ويتم استدعاؤه عندما يتم ربط الـ Fragment بالنشاط المضيف. ()onCreate: ويتم استدعاؤه بعد ربطه بالنشاط مباشرة وذلك لاستخدامها في تهيئة العناصر والمتغيرات التي نرغب في بقائها حتى بعد إزالة واجهة الـ Fragment من داخل النشاط المضيف. ()onCreateView: ويتم استدعاؤه لربط الواجهة الخاصة بالـ Fragment بالشيفرة الخاصة به. ()onActivityCreated: ويتم استدعاؤه بعد انتهاء النشاط المضيف من التابع ()onCreate الخاص به. ()onStart: ويتم استدعاؤه عندما تبدأ الواجهة الخاصة بالـ Fragment في الظهور وذلك بعد الانتهاء من ()onStart الخاصة بالنشاط أولًا. ()onResume: ويتم استدعاؤه عندما تظهر الواجهة الخاصة بالـFragment وتصبح قابلة لتفاعل المستخدم معها، مع العلم بأنه لا يمكن للمستخدم بالتفاعل مع واجهة الـ Fragment قبل أن يتم استدعاء ()onResume الخاصة بالنشاط أولًا لتصبح واجهة النشاط قابلة للتفاعل أيضًا. ()onPause: ويتم استدعاؤه عندما تبدأ الواجهة الخاصة بالنشاط المضيف في الاختفاء أو تظهر بشكل جزئي ولا يمكن للمستخدم التفاعل معها لوجود شيء ما يحجبها، أو عند الاستعداد لاستبدال أو إزالة الـ Fragment. ()onStop: ويتم استدعاؤه عندما تختفي الواجهة الخاصة بالنشاط المضيف من أمام المستخدم أو لاستبدال أو إزالة الـ Fragment. ()onDestroyView: ويتم استدعاؤه عند إزالة الواجهة التي تم إضافتها من قبل في ()onCreateView. ()onDestroy: ويتم استدعاؤه قبل تدمير الواجهة وإزالتها من الذاكرة. ()onDetach: ويتم استدعاؤه عند إزالة الـ Fragment من واجهة النشاط المضيف. لذا فكما ذكرنا فدورة حياة الـ Fragment مشابهة لدورة حياة النشاط ولكن هناك بعض التوابع الخاصة بالـ Fragment فقط ولا تتواجد في النشاط. تطبيق 1 الهدف من هذا التطبيق فهم الأساسيات الخاصة بالـ Fragments حيث تتكون من: واجهة خاصة به (XML File). صنف جديد يرث من Fragment. مكان خاص له في الواجهة الرئيسية الخاصة بالنشاط المضيف. من Android Studio نُنشئ مشروعًا جديدًا يُدعى Simple Fragment وبنفس الإعدادات في المشاريع السابقة. نبدأ أولًا بصنع Fragment جديد (ملف واجهة وملف للشيفرة) عن طريق الضغط بالزر الأيمن على اسم الحزمة الخاصة بالمشروع كما بالصورة التالية: ثم اختر: New > Fragment > Fragment Blank لتظهر لك هذه النافذة، قم بتغيير الاسم إلى الاسم الذي تراه مناسبًا وتغيير الاختيارات كما بالصورة: ثم اضغط Finish. سيقوم Android Studio بعد ذلك بإنشاء صنف جديد يُدعى ExampleFragment ويرث من الصنف Fragment، كما سينشئ ملف واجهة جديد يُدعى fragment_example. نقوم بتغيير ملف الواجهة كما كنا نفعل في التطبيقات السابقة وفي هذا المثال سنكتفي بوضع نص في منتصف الواجهة مع تغيير لون الخلفية: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ccefff" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="Hello From Example Fragment" android:gravity="center"/> </LinearLayout> والآن لربط ملف الواجهة بالشيفرة الخاصة بالصنف ExampleFragment نكتب داخل التابع ()onCreateView -وهو التابع المسؤول عن ربط الشيفرة بالواجهة- الشيفرة التالية: @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView =inflater.inflate(R.layout.fragment_example, container, false); return rootView; } تختلف طريقة ربط الواجهة في الـ Fragment عنها في النشاط (Activity) فلا نستطيع استخدام التابع ()setContentView، عوضًا عن ذلك نستخدم الكائن inflater الذي تم تمريره للتابع ()onCreateView ونستدعي به التابع ()inflate وقد تعاملنا مع هذا التابع من قبل في الدرس الخاص بقوائم العرض ListView، ونمرر للتابع المعاملات الآتية: الواجهة التي نريد ربطها. الواجهة التي ستحتوي واجهة الـ Fragment بداخلها وهي هنا container. قيمة منطقية تُعبر عن "هل نريد وضع واجهة الـ Fragment داخل الواجهة container -المعامل السابق- أم لا؟"، وسنقوم دائمًا عند التعامل مع الـ Fragments بالإجابة بلا أو القيمة المنطقية false؛ وذلك لأنها بشكل افتراضي يتم ربطها. ويقوم هذا التابع بتحويل ملف الواجهة (XML File) إلى كائن من الصنف View يمكن التعامل معه داخل شيفرات جافا بسهولة. بعد ذلك نعيد الكائن rootView إلى التابع، لتصبح الشيفرة النهائية على الشّكل التّالي: package apps.noby.simplefragment; import android.os.Bundle; import android.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class ExampleFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView =inflater.inflate(R.layout.fragment_example, container, false); return rootView; } } لاحظ أنه لكي تعمل الشيفرة بشكل سليم عند الوراثة من الصنف Fragment يجب أن تقوم بتضمين ;import android.app.Fragment. يتبقى الآن الخطوة الأخيرة وهي إيجاد مكان لهذا الـ Fragment داخل النشاط المضيف له، وفي هذا التطبيق النشاط المضيف هو النشاط الرئيسي (MainActivity)، ولوضع مكان للـ Fragment داخل ملف واجهة النشاط نقوم بتعريف عنصر واجهة جديد من النوع <fragment> وتحديد له بعض الخصائص كما يلي: <fragment android:layout_width="match_parent" android:layout_height="match_parent" android:name="apps.noby.simplefragment.ExampleFragment" android:id="@+id/fragment_container" tools:layout="@layout/fragment_example" /> هناك بعض الخصائص المشتركة في العناصر كتحديد الطول والعرض وهناك خصائص تميز العنصر ولابد من تحديدها مثل android:name وتكمن أهميته في تحديد الصنف الذي سيُعرض داخل هذا العنصر ولن يعمل التطبيق بدون وضع قيمة لهذه الخاصية وهنا وضعنا اسم الصنف الذي صنعناه سابقًا مع وضع اسم الحزمة كاملًا قبله. ولابد أيضًا من تحديد id مميز لهذا العنصر. وسنضع داخل ملف الواجهة الرئيسي نص قبل واجهة الـ Fragment ليصبح ملف الواجهة كالتالي: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Main Activity!" android:layout_gravity="center" android:textSize="25sp"/> <fragment android:layout_width="match_parent" android:layout_height="match_parent" android:name="apps.noby.simplefragment.ExampleFragment" android:id="@+id/fragment_container" tools:layout="@layout/fragment_example" /> </LinearLayout> ولن نقوم بتغيير الشيفرة الخاصة بالنشاط الرئيسي وسندعها كما هي، الآن يمكننا تجربة التطبيق على المحاكي للتأكد من عمله كما ينبغي. وتُسمى هذه الطريقة بالطريقة الساكنة لتضمين الـ Fragment؛ وذلك لأننا لم نحتج إلى كتابة شيفرات داخل النشاط لوضع الـ Fragment بداخله. تطبيق 2 في هذا التطبيق سنقوم بعرض أكثر من Fragment بداخل نشاط واحد عند توفر المساحة المناسبة له، أما إذا لم تتوفر فسيتم عرض Fragment واحد فقط. سنقوم بتعديل على المثال السابق وعمل Fragment جديد يُدعى DetailsFragment بنفس الطريقة السابقة ونجعل له الواجهة التالية: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffc829" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="Hello From Details Fragment" android:gravity="center"/> </LinearLayout> وتتشابه الواجهة السابقة مع واجهة الـ Fragment الآخر، بعد ذلك سنقوم بعمل ملف واجهة جديد يُدعى activity_main ولكن سنختار أن يوضع في المجلد layout-large كما في الصورة التالية: وبداخل ملف الواجهة الجديد نضع مكان آخر للـ Fragment الجديد بجانب الأول. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Main Activity!" android:layout_gravity="center" android:textSize="25sp"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <fragment android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:name="apps.noby.simplefragment.ExampleFragment" android:id="@+id/fragment_container" tools:layout="@layout/fragment_example" /> <fragment android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:name="apps.noby.simplefragment.DetailsFragment" android:id="@+id/fragment_details" tools:layout="@layout/fragment_details" /> </LinearLayout> </LinearLayout> ووضع القيم الخاصة بالخاصيتين name و id، والشيفرة الخاصة بملف جافا الخاص بـ DetailsFragment يتشابه مع ExampleFragment. package apps.noby.simplefragment; import android.os.Bundle; import android.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class DetailsFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_details,container,false); return rootView; } } نستطيع الآن تجربة التطبيق بعد إجراء هذا التعديل على محاكي للهاتف وآخر للجهاز اللوحي -إذا لم تكن صنعت محاكيًا للحاسب اللوحي فينبغي أن تصنع واحدًا قبل التجربة-. بناءً على الحجم الخاص بشاشة الجهاز يقوم التطبيق باستدعاء ملف الواجهة الخاص بالنشاط المناسب (العادي أو الكبير). لاحظ أنه يجب أن يكون بجانب اسم المجلد layout كلمة large حتى يعلم التطبيق بوجود ملفات خاصة بالشاشات ذات الحجم الكبير. تطبيق 3 في هذا التطبيق سنقوم بتغيير طريقة تضمين الـ Fragment من الطريقة الساكنة إلى الديناميكية أي إمكانية إضافة، تبديل حذف الـ Fragment أثناء تشغيل التطبيق والتحكم به عن طريق الشيفرة. وللقيام بذلك سنستبدل عنصر الواجهة <fragment> بالعنصر <FrameLayout> والذي يقوم بحجز مساحة فارغة في الواجهة سيتم تحديد فيما بعد ما الذي سيشغلها. كل ما سيختلف في هذا التطبيق ملفات الواجهة الرئيسية الخاصة بالنشاط المضيف فقط ولن يحدث تغيير في الواجهة الخاصة بالـ Fragment. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Main Activity!" android:layout_gravity="center" android:textSize="25sp"/> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/view_container" /> </LinearLayout> ولتحديد المحتوى الخاص بالعنصر FrameLayout ننتقل إلى ملف النشاط MainActivity.java لتعديله، الآن لإضافة Fragment جديد داخل الواجهة ينبغي أن نمر بثلاث خطوات 1. إيجاد كائن من الصنف FragmentManger والذي يستطيع إدارة كافة المهام الخاصة بالتعامل مع الـ Fragments ومن ضمن هذه المهام هي إضافة الـ Framgments ولجلب كائن من هذا الصنف نستدعى التابع ()getFragmentManager حيث يوجد كائن بالفعل داخل النشاط ويكتفي استدعاءه فقط. FragmentManager fragmentManager = getFragmentManager(); 2. للتحكم في المهام الخاصة بإضافة أو تبديل او إزالة الـ Fragments نحتاج إلى كائن من الصنف FragmentTransaction والذي يحتوي على التوابع التي تقوم بعملية الإضافة تلك. FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 3. أخيرًا نستطيع استدعاء التابع المناسب للعملية وتمرير له المعاملات الصحيحة. ExampleFragment frag = new ExampleFragment(); fragmentTransaction.add(R.id.view_container, frag); fragmentTransaction.commit(); يستطيع التابع ()add من إضافة Fragment إلى الواجهة التي سيُعرض بها، وتلك هي المعاملات التي نمررها له: الـ id الخاص بعنصر الواجهة. كائن من الـ Fragment الذي نرغب في عرضه بداخل عنصر الواجهة. وبعد ذلك نستدعي التابع ()commit لتنفيذ العملية السابقة، حيث لن يتم تنفيذها إلا بعد استدعاءه. لتصبح الشيفرة الكاملة للنشاط كما يلي: package apps.noby.simplefragment; import android.app.Activity; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(findViewById(R.id.view_container) != null){ FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); ExampleFragment frag = new ExampleFragment(); fragmentTransaction.add(R.id.view_container, frag); fragmentTransaction.commit(); } } } وفائدة الجملة الشرطية هو التأكد من أننا نتعامل مع ملف الواجهة activity_main.xml الخاص بالهواتف والذي يحتوي بداخله على عنصر له id قيمته view_container، وليس ملف الواجهة الآخر الخاص بالشاشات كبيرة الحجم حيث لا يحتوي على عنصر له ذلك الـ id. والآن بتجربة التطبيق على كلا المحاكيين -الهاتف والحاسب اللوحي- نجد أنه يعمل كالتطبيقات السابقة. تطبيق 4 في التطبيقات السابقة لم نتمكن من عرض الـ Fragment الآخر في حالة الهاتف وذلك لأن التبديل بين Fragment وآخر لا تتم إلا داخل النشاط المضيف، لذا ينبغي أن نجعل الشيفرة الخاصة بالـ Fragment قادرة على التحدث مع النشاط المضيف لها وتنفيذ شيفرات بداخله. ويمكننا القيام بذلك باستخدام interface كحلقة وصل بين النشاط المضيف والـ Fragment. نضع أولًا زر في الواجهة الخاصة بـ fragment_example.xml عند الضغط عليه ننتقل إلى الـ fragment_details كالآتي. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ccefff" android:orientation="vertical" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello From Example Fragment"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Go to Details Frafment" android:id="@+id/go_to_btn"/> </LinearLayout> ثم ننتقل إلى الشيفرة الخاصة بـ ExampleFragment.java وبداخلها نقوم بصنع interface جديد يُدعى OnBtnClicked وبداخله التابع ()goToFragment. public interface OnBtnClicked{ public void goToFragment(); } ثم نجعل الصنف الخاص بالنشاط المضيف يقوم باستخدام هذا الـ interface. public class MainActivity extends Activity implements ExampleFragment.OnBtnClicked نعود مجددًا للشيفرة الخاصة بـ ExampleFragmen.java ونقوم بكتابة التابع ()onAttach الخاص بدورة الحياة للـ Fragment كالتالي: private MainActivity mContext; @Override public void onAttach(Context context) { super.onAttach(context); mContext = (MainActivity) context; } ووظيفة الشيفرة السابقة جعل الشيفرة الخاصة بالـ Fragment تستطيع التواصل مع النشاط المضيف باستخدام كائن منه يُمرر لحظة ربط الـ fragment بالنشاط المضيف. ثم بعد ذلك لربط الزر الذي قمنا بإضافته في الواجهة بالشيفرة نستخدم التابع ()findViewById ولكن هذه المرة باستخدام الكائن rootView وذلك لأنه يُعبر عن الواجهة فلا يمكننا من استدعاء التابع مباشرة كما كان داخل النشاط. @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView =inflater.inflate(R.layout.fragment_example, container, false); Button btn = (Button)rootView.findViewById(R.id.go_to_btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mContext.goToFragment(); } }); return rootView; } ونجعل الزر مستعدًا للاستجابة عند الضغط عليه على أن يقوم باستدعاء التابع ()goToFragment باستخدام الكائن الخاص بالنشاط المضيف. لتصبح الشيفرة النهائية لهذا الـ Fragment كالتالي: package apps.noby.simplefragment; import android.content.Context; import android.os.Bundle; import android.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; public class ExampleFragment extends Fragment { private MainActivity mContext; @Override public void onAttach(Context context) { super.onAttach(context); mContext = (MainActivity) context; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView =inflater.inflate(R.layout.fragment_example, container, false); Button btn = (Button)rootView.findViewById(R.id.go_to_btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mContext.goToFragment(); } }); return rootView; } public interface OnBtnClicked{ public void goToFragment(); } } يتبقى فقط إضافة الوظيفة التي نريدها عن الضغط على هذا الزر وذلك بكتابة الشيفرة الخاصة بالتابع ()goToFragment داخل النشاط المضيف. package apps.noby.simplefragment; import android.app.Activity; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends Activity implements ExampleFragment.OnBtnClicked { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(findViewById(R.id.view_container) != null){ FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); ExampleFragment frag = new ExampleFragment(); fragmentTransaction.add(R.id.view_container, frag); fragmentTransaction.commit(); } } @Override public void goToFragment() { if(findViewById(R.id.view_container) != null){ FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); DetailsFragment frag = new DetailsFragment(); fragmentTransaction.replace(R.id.view_container, frag); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit(); } } } وتتشابه الشيفرة داخل التابع ()goToFragment مع الأخرى المتواجدة داخل ()onCreate ولكننا نستخدم التابع ()replace بدلًا من ()add وذلك لتبديل الواجهات داخل نفس العنصر FrameLayout بشكل ديناميكي، كما نستدعي التابع ()addToBackStack وذلك حتى نضيف واجهة الـ Fragment السابق في الذاكرة الخاصة بزر الرجوع حتى نعود إليها عند الضغط على زر الرجوع وإلا سيتم غلق التطبيق. وتوضح الصورة التالية ما المقصود بذلك: والآن عند تجربة التطبيق على المحاكي (الهاتف أو الحاسب اللوحي) نجد أننا نستطيع الوصول إلى الـ DetailsFragment عند الضغط على الزر. تطبيق 5 في التطبيق التالي سنقوم بإرسال نص من Fragment إلى آخر للتحدث فيما بينهما. ويتم ذلك عن طريق استخدام التابع ()setArguments وللقيام بذلك نقوم بتلك التعديلات على التطبيق السابق. في ملف الواجهة fragment_example.xml نضيف عنصر EditText بالخصائص الآتية. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ccefff" android:orientation="vertical" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello From Example Fragment"/> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="Enter Your Name" android:id="@+id/edit_txt"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Go to Details Frafment" android:id="@+id/go_to_btn"/> </LinearLayout> وفي ملف الواجهة fragment_details نضيف الخاصية id للعنصر TextView. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffc829" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="Hello From Details Fragment" android:gravity="center" android:id="@+id/txt_view"/> </LinearLayout> وفي ملف الواجهة الرئيسي الخاص بالشاشات ذات الحجم الكبير نقوم بتغيير العنصر fragment الثاني بالعنصر FrameLayout. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Main Activity!" android:layout_gravity="center" android:textSize="25sp"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <fragment android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:name="apps.noby.simplefragment.ExampleFragment" android:id="@+id/fragment_container" tools:layout="@layout/fragment_example" /> <FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:id="@+id/fragment_details"/> </LinearLayout> </LinearLayout> داخل الشيفرة الخاصة بـ ExampleFragment.java نقوم بتغيير التابع داخل الـ interface ليقبل تمرير نص، ثم عند الضغط على الزر نرسل النص المكتوب داخل عنصر الواجهة EditText إلى التابع ()goToFragment. package apps.noby.simplefragment; import android.content.Context; import android.os.Bundle; import android.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; public class ExampleFragment extends Fragment { private MainActivity mContext; @Override public void onAttach(Context context) { super.onAttach(context); mContext = (MainActivity) context; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View rootView =inflater.inflate(R.layout.fragment_example, container, false); Button btn = (Button)rootView.findViewById(R.id.go_to_btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { EditText et = (EditText) rootView.findViewById(R.id.edit_txt); String name = et.getText().toString(); mContext.goToFragment(name); } }); return rootView; } public interface OnBtnClicked{ public void goToFragment(String name); } } داخل شيفرة النشاط الرئيسي MainActivity.java سنقوم بإرسال النص المرسل مع الكائن DetailsFragment باستخدام التابع ()setArguments. package apps.noby.simplefragment; import android.app.Activity; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends Activity implements ExampleFragment.OnBtnClicked { public static final String ARGUMENT_NAME = "name"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(findViewById(R.id.view_container) != null){ FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); ExampleFragment frag = new ExampleFragment(); fragmentTransaction.add(R.id.view_container, frag); fragmentTransaction.commit(); } } @Override public void goToFragment(String name) { FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); DetailsFragment frag = new DetailsFragment(); Bundle bundle = new Bundle(); bundle.putString(ARGUMENT_NAME,name); frag.setArguments(bundle); if(findViewById(R.id.view_container) != null){ fragmentTransaction.replace(R.id.view_container, frag); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit(); } else{ fragmentTransaction.replace(R.id.fragment_details, frag); fragmentTransaction.commit(); } } } وتم إضافة حالتين الأولى للواجهات الصغير والأخرى عن التعامل مع الواجهات الكبيرة. أخيرًا نقوم باستقبال النص في الشيفرة الخاصة بـ DetailsFragment.java وعرضه بداخل العنصر TextView. package apps.noby.simplefragment; import android.os.Bundle; import android.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; public class DetailsFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_details,container,false); TextView tv = (TextView) rootView.findViewById(R.id.txt_view); Bundle bundle = getArguments(); String str = bundle.getString(MainActivity.ARGUMENT_NAME); tv.setText("Hello " + str + " From Details Fragment"); return rootView; } } بعد ذلك نقوم بتشغيل التطبيق على المحاكي وتجربته للتأكد من أداءه للوظيفة المطلوبة بشكل سليم. بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  13. سنبدأ في هذا الدرس من سلسلة برمجة تطبيقات الأجهزة المحمولة باستخدام Xamarin بالتعرّف على بنية التطبيق ضمن الحل Solution في Visual Studio 2015، بالإضافة إلى إنشائنا لتطبيق بسيط نتعرّف من خلاله على آلية عمل التطبيقات المبنية بواسطة Xamarin والتي تشبه في العديد من الجوانب مثيلاتها المنشأة باستخدام التقنيّات الأساسيّة المعتمدة في إنشاء تطبيقات أندرويد (لغة جافا مع Android Studio). بنية التطبيق المنشأ باستخدام Xamarin شغّل برنامج Visual Studio 2015، وبعد ظهور النافذة الرئيسيّة انتقل إلى القائمة File ومنها إلى New ثم اختر من القائمة الفرعيّة التي ستظهر الخيار Project. وكما فعلنا في الدرس السابق، اختر من الجهة اليسرى للنافذة التي ستظهر Cross-Platform، ثمّ اختر المشروع (Blank App (Xamarin.Forms Portable من القسم الأوسط للنافذة. امنح هذا المشروع الاسم SimpleTextApp ثم انقر الزر OK. سنبني تطبيقًا بسيطًا يوضّح كيفيّة استخدام لُصيقة Label وحيدة في إظهار النص في التطبيقات المبنيّة باستخدام Xamarin. من مستكشف الحل Solution Explorer الذي يوجد عادةً في الجهة اليمنى لنافذة بيئة التطوير Visual Studio، أبقِ فقط على المشروعين SimpleTextApp.Droid و (SimpleTextApp (Portable لأنّنا سنتعامل مع تطبيقات أندرويد فحسب (كما اتفقنا أيضًا من الدرس السابق). احذف باقي المشاريع الإضافيّة، يجب أن تحصل على شكل شبيه بما يلي: يحتوي المشروع (SimpleTextApp (Portable على معظم الشيفرة البرمجيّة، وهذا سلوك عام في المشاريع ذات النمط PCL. حيث تُعتبر الشيفرة الموجودة ضمن المشروع (الذي يحمل المقطع Portable بالإضافة لاسمه) مشتركة لجميع أنواع التطبيقات العاملة على أيّ نظام تشغيل بما فيه أندرويد. نلاحظ الملف App.cs الأساسي الذي سيكون موجودًا في أيّ تطبيق Xamarin وقد تحدثنا عنه في الدرس السابق ورأينا كيف يحتوي على الصنف App الذي يرث من الصنف Application، وكيف أنّ بانية هذا الصنف هي المسؤولة عن إنشاء الصفحة الرئيسيّة للتطبيق. انشر العقدة References لترى المراجع الخاصّة بهذا المشروع. ستلاحظ وجود عدّة مكتبات يهمّنا منها: Xamarin.Forms.Core و Xamarin.Forms.Platform و Xamarin.Forms.Xaml هذه هي المكتبات الرئيسيّة المشكّلة لـ Xamarin.Forms. بالنسبة للمشروع SimpleTextApp.Droid فهو يحتوي على مجلّدين هما: Assets و Resources. بالنسبة للمجلّد Resources فيحتوي على ملفات الصور وملفات معلومات التخطيطات layouts descriptions وغيرها من الملفات التي تُعتبر بمثابة المصادر resources لتطبيقك. انشر المجلّد Resources لترى بنيته. أمّا بالنسبة للمجلّد Assets فتُوضَع ضمنه الملفات العامّة الأخرى التي ترغب بتضمينها مع تطبيقك. يحتوي هذا المشروع أيضًا على الملف MainActivity.cs الذي يحوي ضمنه صنف له نفس الاسم MainActivity وهو يمثّل الفعاليّة Activity الأساسيّة لتطبيق أندرويد. افتح هذا الملف لتجد أنّ هذا الصنف يحتوي على التابع OnCreate الذي يُنفَّذ عند بدء تشغيل تطبيق أندرويد. لاحظ العبارة البرمجيّة التالية ضمن هذا التابع: LoadApplication(new App()); من الواضح أنّها تنشئ كائنًا جديدًا من الصنف App الموجود ضمن المشروع (SimpleTextApp (Portable، ثم تمرّره إلى التابع LoadApplication مباشرةً ليعمل على تحميل التطبيق وإظهاره للمستخدم. إذًا فعند تشغيل تطبيق أندرويد، سينفّذ التابع OnCreate الموجود في الصنف MainActivity أولًا، حيث يعمل هذا التابع على إنشاء كائن جديد من الصنف App فينفّذ بانيته وهي المسؤولة عن إنشاء الصفحة الرئيسيّة للتطبيق كما أشرنا. على أيّة حال ستتوضّح الفكرة بشكل جيّد بعد أن نبدأ بكتابة الشيفرة البرمجيّة. كتابة الشيفرة البرمجية انقر بزر الفأرة الأيمن على اسم المشروع (SimpleTextApp (Portable في مستكشف الحل Solution Explorer لتظهر قائمة سياق، اختر منها Add ثمّ من القائمة الفرعيّة اختر Class. ستظهر النافذة الخاصّة بإضافة صنف جديد. من حقل الاسم Name من الأسفل اكتب الاسم التالي لهذا الصنف الجديد وهو MainPage، ثم انقر Add. سيعمل Visual Studio على إضافة ملف جديد إلى المشروع اسمه MainPage.cs يحتوي على صنف بنفس اسمه ومحتواه شبيه بما يلي: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SimpleTextApp { class MainPage { } } لن نحتاج إلى معظم نطاقات الأسماء الموجودة في الشيفرة السابقة في هذا البرنامج، سنحتاج حاليًا إلى نطاق الاسم Xamarin.Forms. وسنجعل الصنف MainPage يرث من الصنف ContentPage كما سنضيف البانية إليه. عدّل الشيفرة السابقة لتصبح على الشكل التالي: using Xamarin.Forms; namespace SimpleTextApp { class MainPage : ContentPage { public MainPage() { } } } لاحظ أنّني تخلصت من جميع نطاقات الأسماء غير الضرورية (على أية حال سيظهر أي نطاق اسم غير مُستَخدَم في الشيفرة البرمجيّة بلون باهت في بيئة التطوير Visual Studio 2015). سيلعب الصنف MainPage دور صفحة محتوى content page رئيسيّة للتطبيق SimpleTextApp. سنضع ضمن بانية هذا الصنف الشيفرة البرمجيّة اللازمة لإنشاء لُصيقة واحدة Label تحوي نصًّا بسيطًا. ثم سنجري بعض التجارب البسيطة على هذه اللُّصيقة. يُعتبر هذا البرنامج بسيطًا في الواقع، ومهمته جعلك أكثر تآلفًا مع Xamarin. سنبدأ بإنشاء لُصيقة من الصنف Label وذلك ضمن بانية الصنف MainPage. تُستخدم اللُصيقات عادةً لعرض النصوص إلى المستخدم. انظر إلى الشيفرة البسيطة التالية: Label lblMain = new Label(); lblMain.Text = "Welcome to Hsoub Academy!"; ننشئ لُصيقة كما ننشئ أيّ كائن من صنف ما. أنشأنا في الشيفرة السابقة كائنًا جديدًا من الصنف Label وأسندناه إلى المتغيّر lblMain. ثم أسندنا النص "!Welcome to Hsoub Academy" إلى الخاصيّة Text من المتغيّر lblMain. تُستَخدم الخاصية Text لضبط وقراءة المحتوى النصيّ للُّصيقة. كي نستطيع عرض اللُّصيقة السابقة على الصفحة الرئيسيّة للتطبيق، علينا إسناد المتغيّر lblMain إلى الخاصيّة Content من الصنف ContentPage. وبما أنّ الصنف MainPage يرث من ContentPage فستكون الخاصيّة Content موجودة تلقائيًّا ضمنه أيضًا. ستكون عمليّة الإسناد على الشكل التالي: this.Content = lblMain; تكون عمليّة الإسناد السابقة ممكنة لأنّ الصنف Label يرث من الصنف View الذي يمثّل واجهة عرض view بمفهومها العام. وبما أنّ الخاصيّة Content هي من النوع View أيضًا فتكون عمليّة الإسناد هذه ممكنة. ستكون محتويات الملف MainPage.cs شبيهة بما يلي: using Xamarin.Forms; namespace SimpleTextApp { class MainPage:ContentPage { public MainPage() { Label lblMain = new Label(); lblMain.Text = "Welcome to Hsoub Academy!"; this.Content = lblMain; } } } هذا كلّ شيء! علينا الآن القيام بخطوة صغيرة أخيرة، وهي في جعل تطبيقنا يُنشئ كائن من صنفنا الجديد MainPage وذلك ضمن الملف App.cs. انتقل إلى هذا الملف (موجود ضمن المشروع (SimpleTextApp (Portable واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new MainPage(); } اضغط الآن المفتاح F5 لتنفيذ التطبيق، لتحصل على شكل شبيه بما يلي: ملاحظة يمكنك وصل جهازك المحمول الذي يعمل بنظام أندرويد إلى الحاسوب لتجريب تشغيل التطبيق عليه مباشرةً. ولكن في هذه الحالة ينبغي أن تتوفّر واجهة برمجيّة API منصّبة على حاسوبك مناسبة لنسخة أندرويد الموجودة على الجهاز المحمول. على أية حال يمكنك تنصيب هذه الواجهة في حال عدم وجودها من خلال Android SDK Manager كما أشرنا في الدرس السابق. كما أنصح بوصل الجوال قبل تشغيل بيئة التطوير Visual Studio، أو أن تعيد تشغيلها إذا كانت تعمل من قبل. تنسيق الشيفرة البرمجية بشكل أفضل في الحقيقة لا تُعتبر الشيفرة البرمجيّة الموجودة ضمن الملف MainPage.cs مثاليّةً رغم بساطتها. ربما لا تشعر بمشكلة الآن، ولكن عندما تصبح برامجك أكبر، ستقع في مشاكل حقيقيّة لأنّ الأسلوب السابق بكتابة الشيفرة يجعلها كبيرة جدًّا. سأستخدم ميّزة الإسناد المباشر للخصائص أثناء إنشاء الكائنات. تفيد هذه الميزة في الاستغناء عن معظم المتغيّرات في البرنامج، التي ينحصر دورها في لعب دور الوسيط المؤقّت. سنعيد صياغة الشيفرة الموجودة في الملف MainPage.cs بشكل أنيق وعمليّ أكثر: using Xamarin.Forms; namespace SimpleTextApp { class MainPage : ContentPage { public MainPage() { this.Content = new Label{ Text = "Welcome to Hsoub Academy!" }; } } } لاحظ أنّني استغنيت عن المتغيّر lblMain لأنّنا أسندنا كائن اللُّصيقة مباشرةً إلى الخاصيّة Content. مزايا إضافية على التطبيق ستلاحظ من التطبيق السابق أنّ اللُّصيقة تظهر في الزاوية اليسرى العليا من النافذة. سنجعلها تظهر في مركز الشاشة. ينبغي ضبط خاصيّتين من خصائص اللُّصيقة لهذا الغرض، وهما HorizontalOptions (لخيارات المحاذاة الأفقيّة) و VerticalOptions (لخيارات المحاذاة الرأسيّة). كل من هاتين الخاصيّتين من النوع LayoutOptions، وهي بنية struct موجودة ضمن نطاق الاسم Xamarin.Forms. تحتوي هذه البنية على عدّة حقول جاهزة تُفيد في خيارات التموضع الأفقيّة والرأسيّة للُّصيقة، وهي خيارات مهمّة للغاية كما سنرى لاحقًا في هذه السلسلة. لنجري الآن التعديل المطلوب على اللُّصيقة الموجودة ضمن بانية الصنف MainPage. ستصبح محتويات الملف MainPage.cs على الشكل التالي: using Xamarin.Forms; namespace SimpleTextApp { class MainPage : ContentPage { public MainPage() { this.Content = new Label{ Text = "Welcome to Hsoub Academy!", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; } } } لاحظ إسناد القيمة LayoutOptions.Center لكل من الخاصّتين HorizontalOptions و VerticalOptions وذلك للتوسيط الأفقي والرأسي على الترتيب. نفّذ البرنامج مرّة أخرى ستلاحظ أنّ اللُّصيقة أصبحت في مركز الشاشة كما هو واضح من الشكل التالي: أمّا إذا أحببت أن يكون النص ضمن اللُّصيقة بلون آخر، وليكن الأحمر مثلًا، فعليك في هذه الحالة إسناد القيمة Color.Red للخاصيّة TextColor لكائن اللُّصيقة، علمًا أنّ Color هو بنية موجودة ضمن نطاق الاسم Xamarin.Forms أيضًا. انظر كيف نفعل ذلك في الشيفرة: this.Content = new Label{ Text = "Welcome to Hsoub Academy!", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, TextColor = Color.Red }; نفّذ البرنامج مرّة أخرى، لترى النص وقد أصبح لونه أحمر. الخلاصة تعرّفنا في هذا الدرس على البنية العامة لتطبيق Xamarin ضمن بيئة التطوير Visual Studio 2015. كما قمنا ببناء تطبيق بسيط للغاية أسميناه SimpleTextApp. تنحصر وظيفة هذا التطبيق في التّعرف أكثر على Xamarin. سنبدأ اعتبارًا من الدرس القادم ببناء تطبيقات متدرّجة في الصعوبة، وذات مزايا إضافيّة من الممكن أن تستفيد منها كمبرمج تطبيقات محمولة في حياتك العمليّة.
  14. سنتابع العمل في سلسلة برمجة تطبيقات الأجهزة المحمولة باستخدام Xamarin، حيث سنعمل في هذا الدرس على تنزيل وتنصيب برنامج Visual Studio 2015 Community الذي يأتي بشكل مجّاني من مايكروسوفت. حيث أنّ منصّة Xamarin أصبحت تأتي مع Visual Studio كميّزة من المزايا الاختياريّة التي من الممكن اختيارها أثناء تنصيب Visual Studio، وذلك بعد استحواذ مايكروسوفت على شركة Xamarin. تنزيل وتنصيب Visual Studio 2015 Community أنصح أن يكون التنصيب على ويندوز 10 أو ويندوز 8.1. انتقل إلى صفحة تنزيل بيئة التطوير Visual Studio 2015. كما في الشكل التالي: سنختار الإصدار Community من اليسار، لذلك فانقر الزر Download Community Free. سيعمل المتصفّح على تحميل ملف تنفيذي صغير اسمه vs_community.exe وهو برنامج الإعداد الذي سيعمل على تنصيب Visual Studio Community. سيشّغل المتصفّح هذا البرنامج بعد تنزيله فورًا، لذلك سيعطيك ويندوز تحذير أمان أنّه برنامج تنفيذي، ويعرض عليك تشغيله أو إلغاء العمليّة. اقبل تشغيله من خلال نقر الزر Run كما في الشكل التالي: سيعمل برنامج الإعداد ويبدأ بجلب بيانات التنصيب من الإنترنت. قد يستغرق ذلك القليل من الوقت قبل أن تحصل على النافذة في الشكل التالي، التي تخيّرنا بين خيارين للتنصيب: افتراضي Default ومخصّص Custom: انقر الخيار المخصّص Custom ثمّ انقر Next. لينتقل برنامج الإعداد إلى نافذة المزايا المطلوب تنصيبها. انتقل إلى العقدة Cross Platform Mobile Development وانشرها لتصل إلى الميّزة (C#/.NET (Xamarin v4.0.4 وهو الإصدار الحالي لمنصّة Xamarin حين كتابة هذا الدرس. قد يختلف هذا الإصدار بالنسبة إليك. انقر صندوق الاختيار بجوار هذه الميّزة، سيؤدّي ذلك إلى اختيار مزايا أخرى بشكل تلقائي. في الحقيقة سيكون حجم حزمة البيانات التي ستُنزّل من الإنترنت كبيرة نسبيًّا. بالنسبة لهذه السلسلة لن نحتاج إلى جميع هذه المزايا، لذلك انتقل إلى العقدة Programming Languages وانشرها، وأزل الإشارة من صندوق الاختيار الموجود بجانب ++Visual C. انقر الزر Next لنصل إلى المرحلة النهائيّة قبل البدء بعمليّة التنصيب، وهي المرحلة التي تلخّص ما سيقوم به برنامج الإعداد. انقر الزر Install لتبدأ عمليّة التنصيب التي ستأخذ بعض الوقت بحسب سرعة الإنترنت لديك. ستتضمّن عملية التنصيب تحميل حزمة التطوير البرمجيّة SDK الخاصة بأندرويد، بالإضافة إلى تثبيت واجهتين برمجيّتين أو أكثر افتراضيًّا مثل API 19 و API 21. على العموم يمكن تثبيت الواجهات البرمجيّة التي ترغبها بعد انتهاء التنصيب وذلك من خلال مدير الحزم والواجهات في أندرويد Android SDK Manager الذي يأتي مع حزمة التطوير البرمجيّة SDK. اقرأ هذا المقال هنا على أكاديميّة حسّوب للمزيد من المعلومات حول الواجهات البرمجيّة API ودعمها للأجهزة المشغّلة لأندرويد. التشغيل الأول لبيئة التطوير Visual Studio عند تشغيل بيئة التطوير Visual Studio 2015 للمرّة الأولى، سيطلب منك Visual Studio تسجيل الدخول باستخدام حساب بريد إلكتروني من مايكروسوفت (كحساب بريد إلكتروني على Hotmail مثلًا) كما في الشكل التالي: انقر الزر Sign in لتسجيل الدخول، لتحصل على النافذة الخاصّة بإدخال البريد الإلكتروني. أدخل البريد الإلكتروني ثم انقر Continue وإذا طلب منك Visual Studio أن تحدّد نوع البريد الإلكتروني، فاختر شخصيّ Personal Email. بعد ذلك ستصل إلى نافذة تطلب منك كلمة المرور لحساب البريد الإلكتروني الذي أدخلته قبل قليل. أدخل كلمة المرور، ثم انقر Sign in، لتصل إلى النافذة الرئيسيّة لتطبيق Visual Studio كما في الشكل التالي: إنشاء مشروع جديد من النافذة الرئيسيّة لبيئة التطوير Visual Studio، انقر القائمة File من الأعلى، واختر منها New ثم Project لإنشاء مشروع جديد، ستحصل على النافذة التالية: اختر من الشجرة التي تظهر على الجانب الأيسر الخيار: Cross-Platform (قد تحتاج لنشر بعض العقد لتصل إليه). ثم اختر نوع المشروع Blank App Xamarin.Forms) Portable) من القسم الأوسط للنافذة وذلك من أجل اختيار مكتبة الأصناف المحمولة PCL مع هذا التطبيق. ثمّ أدخل الاسم HelloWorld في حقل الاسم Name من الأسفل، ثم انقر OK. سيستغرق الأمر وقتًا قليلًا ليعمل Visual Studio على إنشاء عدّة مشاريع ضمن الحل Solution الحالي. بالنسبة إليّ (أستخدم نظام التشغيل Windows 10) فقد حصلت على خمسة مشاريع ضمن هذا الحل وهي: (HelloWorld (Portable HelloWorld.Droid HelloWorld.iOS (HelloWorld.Windows (Windows 8.1 (HelloWorld.WinPhone (Windows Phone 8.1 تظهر هذه المشاريع ضمن مستكشف الحل Solution Explorer في الجانب الأيمن من النافذة (إذا لم يكن ظاهرًا فيمكنك إظهاره من القائمة View ثم اختيار Solution Explorer). انظر الشكل التالي: في الحقيقة لن نهتم في هذه السلسلة سوى بالتطبيقات التي تعمل على أندرويد، لذلك سنحتفظ بالمشروعين (HellowWorld (Portable و HellowWorld.Droid ونحذف باقي المشاريع. انقر بزر الفأرة الأيمن على المشروع HelloWorld.iOS ثم اختر Remove لإزالته. كرّر نفس العمليّة بالنسبة للمشروعين (HelloWorld.Windows (Windows 8.1 و (HelloWorld.WinPhone (Windows Phone 8.1. ملاحظة: قد تختلف المشاريع التي تظهر عندك بشكل طفيف. على أيّة حال احرص على وجود مشروعين فقط وهما HelloWorld.Droid و (HelloWorld (Portable. تشغيل تطبيق أندرويد الأول لتشغيل تطبيقات أندرويد فإنّنا نحتاج إلى جهاز ذكي يشغّل أندرويد (بصرف النظر عن الإصدار) أو أن يتوفّر لدينا محاكي Emulator يعمل على محاكاة عمل هذا الجهاز ولكن على حاسوبنا الشخصي. توفّر مايكروسوفت محاكٍ Emulator خاص بها: Visual Studio Emulator For Android، وذلك لمحاكاة عمل تطبيقات أندرويد على جهاز الحاسوب بدون الحاجة إلى وجود جهاز فيزيائي متصل بالحاسوب. يُعتبر هذا المحاكي برأيي أفضل من المحاكي الافتراضي الذي يأتي مع حزمة التطوير الخاصّة بأندرويد من حيث الأداء. وعلى أية حال، فستحتاج إلى تفعيل ميزة HAXM التي تأتي مع معالجات Intel، والتي يحتاجها المحاكي الافتراضي لتسريع أدائه. انظر إلى هذا الرابط لتعرف المزيد عن هذا الموضوع. لنشغّل تطبيقنا الأوّل ضمن المحاكي الخاص بمايكروسوفت، سيظهر لك أعلى نافذة بيئة التطوير شريط صغير يحتوي على اسم المحاكي الذي سيتم تشغيله. انظر إلى الشكل التالي: لاحظ بأنّ المحاكي الذي لديّ يدعم الواجهة البرمجيّة API 19 ولا بأس في ذلك. يمكنك دعم أي واجهة برمجيّة ترغبها، باستخدام Android SDK Manager. كلّ ما فعلته أنّني تركت الإعدادات الافتراضيّة كما هي. انقر السهم الأخضر الصغير للتشغيل في وضع التنقيح debugging mode (أو يمكنك اختيار الأمر Start Debugging من القائمة Debug أو اضغط F5). سيستغرق الأمر قليلًا من الوقت حتى تحصل على شكل شبيه بما يلي: لاحظ الرسالة الترحيبيّة !Welcome to Xamarin Forms في وسط الشاشة. مبروك لقد حصلت على برنامجك الأوّل! أوقف الآن تشغيل البرنامج باختيار الأمر Stop Debugging من القائمة Debug (أو اضغط Shift+F5). انتقل إلى مستكشف الحل Solution Explorer وانشر المشروع (HelloWorld (Portable ثمّ انقر على الملف App.cs وهو الملف الأساسيّ في أيّ تطبيق من تطبيقات Xamarin. ستجد ضمنه الشيفرة التالية: 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 using Xamarin.Forms; 7 8 namespace HelloWorld 9 { 10 public class App : Application 11 { 12 public App() 13 { 14 // The root page of your application 15 MainPage = new ContentPage 16 { 17 Content = new StackLayout 18 { 19 VerticalOptions = LayoutOptions.Center, 20 Children = { 21 new Label { 22 XAlign = TextAlignment.Center, 23 Text = "Welcome to Xamarin Forms!" 24 } 25 } 26 } 27 }; 28 } 29 30 protected override void OnStart() 31 { 32 // Handle when your app starts 33 } 34 35 protected override void OnSleep() 36 { 37 // Handle when your app sleeps 38 } 39 40 protected override void OnResume() 41 { 42 // Handle when your app resumes 43 } 44 } 45 } لاحظ الصنف App في السطر 10. يرث هذا الصنف من الصنف Application الذي يمثّل التطبيق في Xamarin.Forms. أيّ تطبيق ننشئه في Xamarin.Forms يجب أن يرث من هذا الصنف. تقع بانية الصنف App في الأسطر من 12 حتى 28 وفيها يتمّ تعيين الصفحة الرئيسيّة التي سيظهرها التطبيق عند تشغيله. أيّ صفحة من صفحات التطبيق ستكون عبارة عن كائن من صنف يرث من الصنف ContentPage (صفحة محتوى). يمكن أن تحتوي هذه الصفحة على أيّ شيء يخطر ببالك من مربّعات النص والأزرار وأشرطة التمرير وغيرها من الأدوات. لاحظ أنّنا في السطر 15 (ضمن بانية الصنف App) قد أسندنا كائنًا جديدًا من الصنف ContentPage إلى الخاصيّة MainPage وهي خاصيّة تتبع الصنف Application (الذي يرث منه الصنف App). تستخدم الشيفرة الموجود هنا أسلوب الإنشاء المختصر للكائنات، فكلّ شيء يتمّ هنا من خلال عبارة برمجيّة واحدة. حيث نسند الخصائص للكائنات الجديدة المُنشأة مباشرةً عند إنشاء هذه الكائنات. فمن خلال الشيفرة الموجودة في الأسطر بين 15 و 27 كرّرنا هذا الأسلوب ثلاث مرّات، وذلك من أجل كائنات جديدة من الأصناف ContentPage و StackLayout و Children. ملاحظة يُعتبر الإسناد المختصر للخصائص عند إنشاء كائنات جديدة، من التقنيّات المهمّة التي تبسّط الشيفرة البرمجيّة إلى حدّ كبير. وكيف تنعش ذاكرتك، إليك المثال البسيط التالي. بفرض أنّه لدينا الصنف Student الذي يحتوي على الخصائص التالية: FirstName و LastName و Age. عند إنشاء كائن جديد من الصنف Student يمكننا استخدام العبارة البرمجيّة التالية: Student student = new Student { FirstName = "Ahmad", LastName = "Shareef", Age = 16 }; ففي هذه الحالة ننشئ كائن جديد من الصنف Student وبنفس الوقت نُسند القيم المناسبة لخصائصه. سنؤجّل الحديث عن التوابع OnStart و OnSleep و OnResume لنتحدّث عنها لاحقًا في هذه السلسلة، رغم أنّ أسمائها توحي بوظائفها التي تُعتبر مفيدة ومهمّة في عمل التطبيق. الخلاصة تعلّمنا في هذا الدرس كيفيّة تنزيل وتنصيب Visual Studio 2015 Community مع إضافة قابليّة تطوير تطبيقات لأندرويد باستخدام Xamarin. لاحظ أنّني قد تركت الأمور تسير بشكلها الافتراضيّ قدر المستطاع، لأنّه كما هو واضح هناك العديد من الإجراءات كي تصبح بيئة التطوير جاهزة للعمل، وللبدء بتطوير تطبيقات تعمل على أندرويد وغيره من أنظمة التشغيل باستخدام سي شارب #C و Xamarin.
  15. لقد صار التصميم الماديّ (material design) شائعًا جدًّا هذه الأيام، حيث بدأ كثير من المصممين بتضمينه في تصاميمهم ليس فقط لتطبيقات أندرويد، بل ولمشاريع الويب أيضًا. لأذكّرك، لقد تم طرح المفهوم لأول مرة من طرف جوجل في Google I/O 2014 keynote. لقد شوهد هذا العرض التقديميّ أكثر من 1.5 مليون مرة حتى الآن، وما زال يتم التعامل معه على أنه العرض الأساسيّ لماهيّة التصميم الماديّ وكيف علينا أن نتخيله. هل التصميم الماديّ مناسب لك؟السؤال الأول الذي سنحاول الإجابة عليه الآن هو ما إذا كان التصميم الماديّ شيء عليك تعلمه والبدء باستخدامه في عملك. ولكن، كما هي العادة عندما يتعلق الأمر بهذه الأسئلة، ليست هناك إجابة واحدة تناسب الجميع. لنحاول إذًا أخذ هذه المسألة من زاوية أخرى. هناك أشياء أنا متأكد من أنك ستوافقني عليها، ومنها أن التصاميم الرائعة هي تصاميم مميزة وتقوم بمهمتها على أكمل وجه. وقد يكون أداء التصميم دوره على اكمل وجه أهم من أي أمر آخَر. وبعبارة أخرى، جمالية التصميم لمجرد أن يكون التصميم جميلًا أمر لا جدوى منه. ولهذا، فعند التفكير في تبني تصميم material، حاول أولًا أن تربطها بالأهداف التي ترغب بتحقيقها من تصميمك. وبشكل أساسي، أجب الأسئلة التالية بنفسك: هل تتوافق مبادئ وتوجيهات التصاميم المادية مع ما أريد تحقيقه؟ ملاحظة: ربما تكون قراءة مقالنا السابق حول الاختلافات بين التصاميم المسطحة والتصاميم المادية فكرة جيدة قبل الاستمرار في قراءة هذا المقال. لقد تطرّقنا فيه إلى أكبر الاختلافات بين التصاميم المسطحة والتصاميم الماديّة، ومزايا ومساوئ كلّ منها. سيقدم لك هذا المقال مساعدة إضافية عند اختيارك لتوجّه معين. 1. تعرف على المصدر الرئيسيإذا رغبت بتعلم التصاميم الماديّة، فأفضل نقطة تبدأ بها هي المصدر الرسميّ الذي نشرته جوجل. يتم تحديث هذا المصدر باستمرار، ويشرح كلّ التفاصيل الصغيرة التي تندرج ضمن بناء تصاميم ماديّة. والجميل فيه هو أنه لا يغطي النواحي المتعلقة بأندرويد من التصاميم المسطّحة وحسب، بل ويناقش أيضًا التصاميم المسطّحة بشكل عام وعلاقتها بأيّ مشروع موقع أو تطبيق. أنصحك بشدة أن تقرأ على الأقل الفصول الأولى من التوثيق لتتعرف على المفاهيم الأساسيّة. 2. افهم معنى "ماديّ" في التصميم الماديّإن مفهوم "تصميم ماديّ" لم يأتِ اعتباطًا. ببساطة، يشير مفهوم التصاميم الماديّة إلى جعل التصاميم تمثل العالم الحقيقي، ولكن على قدر معين من التجريد. بالطبع لا تريد أن تجعل تصميمك يبدو واقعيًّا أكثر من اللازم إلى مرحلة ينتحل فيها شكل عنصر معين في العالم الحقيقيّ. إليك الفكرة. نحن -البشر- نفهم الأشياء الماديّة. نعرف ملمس المعدن، ونعرف الشكل الذي يكون عليه المكتب الخشبيّ. يمكننا أيضًا أن نميّز أشياء موضوعة فوق أشياء أخرى. يمكننا مثلًا أن نميّز قلم رصاص موضوع على ورقة موضوعة على مكتب. ببساطة، ستتعلم في التصميم الماديّ أن تعبر عن الترتيب نفسه للعناصر، ولكن بأن تستخدم فقط أقل ما يمكن من عناصر التصميم، كالظلال مثلًا. مصدر الصورة: ظلّ - لـNikhil Vootkur من Behance. 3. استخدم الظلال لتوصل مفهوم التقسيم الشجريّإن السطوح والحواف والظلال والإضاءة هي الأدوات الرئيسيّة للتصاميم الماديّة. إن إضافة العمق لتصاميم أمر هامّ جدًّا، ولكن عليك أن تنتبه لأن تستخدم الحدّ الأدنى من الجرعة الفعالة منه. فمثلًا، الظلال هي الأداة الرئيسيّة لديك للتعبير عن هرميّة العديد من العناصر التي تجتمع معًا لتشكل التصميم الكامل. عندما تقرر ما الذي يُلقي ظلًّا صغيرًا واقعيًّا على شيء آخر، فإنك تُبرز الهرميّة المرئيّة للعناصر والطبقات التي هي موضوعة عليها. إن ما يهم هنا هو التكوين العام للتصميم، وما إذا كانت الظلال لديك ككل منطقيّة بالنسبة للناس، وإن كانت تُبدي مفهوم الأشياء الماديّة الحقيقيّة. مصدر الصورة: WhatsApp | Material Design Concept - لـMário Gomide من Behance. 4. استخدم ألوان جريئةثلاثة من المبادئ الرئيسيّة للتصاميم الماديّة هي أن تكون جرئية، وجميلة التصميم، وعالميّة. التصاميم الماديّة تصاميم تعتمد على الحدّ الأدنى من التصميم دون مبالغات بالتأكيد. وبعبارة أخرى، فإنها لا تستخدم الكثير من أدوات التصميم والجماليات. ولهذا، فعلى المصممين أن يلتفوا على هذه القيود وأن يجدوا طرق أخرى لإنتاج معنى وجذب الانتباه إلى إنشاء التركيز المناسب. إن الألوان من الأشياء القليلة المتروكة للمصمم. ولنتكلم بدقة، الألوان الجريئة، وغالباً الألوان الصاخبة. للألوان الجرئية دور مهم في التصاميم الماديّة (وفي التصاميم المسطّحة أيضًا). تجعل هذه الألوانُ الأشياءَ ممتعة، وتجعل المستخدم يستمتع بالتفاعل مع التصميم. مثال على تصميم ملون: مصدر الصورة: Behance New App Concept (Material Design - لـ Fabrizio Vinci من Behance. 5. استخدم لونا أساسيا وآخر ثانوياتقول مستندات جوجل الرسميّة: "حدّد اختيارك للألوان باختيار ثلاثة ألوان من منقاة الألوان (palette) الرئيسيّة، ولونًا ثانويًا من منقاة الألوان الثانوية." قد تكون طريقة تطويع هذه لأي نوع من التصميم كالتالي: استخدم ثلاثة ألوان لتشكل منقاة الألوان الرئيسيّة لك، وانتقِ لونًا آخر ليكون لونًا ثانويًّا. يمكن أن تستخدم لونك الرئيسيّ لأشياء كالخلفية وأماكن الإدخال والصناديق والخطوط والعناصر الأخرى الرئيسيّة في الواجهة. أما اللون الثانوي، -فهو كما يشير اسمه- يعطي عمقًا إضافيًّا عندما ترغب بعرض العناصر الرئيسيّة على صفحة أو شاشة معينة. سيكون من الضروريّ بالتأكيد أن يكون اللون الثانوي متباينًا بشدّة مع الألوان الرئيسيّة. مصدر الصورة: Snapchat – Material Design - لـ Vinfotech من Behance. 6. استخدم المساحات الفارغةيأخذ التصميم الماديّ الكثير من الأفكار من تصاميم الطباعة التقليدية والمفاهيم التي أتتنا بها. فمثلًا، المساحات الفارغة جزء مهم لأيّ تصميم ماديّ، ويمكنها أن تحسن الخطوط وشكل النصوص بقدر هائل. في الواقع، المساحات الفارغة هي أهم الأدوات لإنشاء تركيز ولجذب انتباه المستخدم إلى عنصر معين (وهو شيء غالبًا يجد المصممون المبتدئون صعوبة في فهمه). لذ -باختصار- استخدم خطوطًا كبيرة للعناوين، وأضف الكثير من المساحات البيضاء، ولا تخش أن تضيف الكثير من المساحات الفارغة في تصاميمك بشكل عام. مصدر الصورة: Material Design Sign Up Page - لـ Omkar Abnave من Behance. 7. استخدم الصور من الحافة إلى الحافةإن التصاميم الماديّة صديقة للصور للغاية. ما أعنيه هنا هو أنك إذا قررت تضمين أي صور في تصميمك، فعليك أن تعطيها دورًا مهمًا. إن الصور في التصاميم الماديّة تُستخدم من الحافة إلى الحافة، في ما يعرف بطريقة full-bleed. يعني هذا عدم وجود حاشية بين حافة الصورة وحافة الشاشة. عند عمل هذا بشكل سليم، سيعطي تجربة فريدة للمستخدم، ويعطينا نحن المصممين بعض الأدوات الإضافية إضافة إلى المجموعة الصغيرة من الظلال المسموح بها مسبقًا، والألوان، والطبقات. مصدر الصورة: Twitter Material Design - لـ Rico Monteiro على Behance. 8. للتصاميم المعتمدة على الصور، استخرج الألوان من الصوربالحديث عن الصور، تنصحنا جوجل باستخراج الألوان من الصور التي نستخدمها في تصاميمنا ومن ثم نجعلها جزءًا من مجموعة الألوان لدينا. يصعب أن تجد حجّة تناقض فيها هذا المفهوم. إن اتباع هذه الطريقة سيعطي تجربة متزنة للمستخدم بكل تأكيد، وسيعطي انطباعًا بأن كل شيء في مكانه المناسب، وأن العناصر الموجودة كلها متسقة مع بعضها. مصدر الصورة: Material Design Colors Recontextualization - لـ Lonely Pig Ringo على Behance. 9. استخدم الحركاتوبحسب تعبير جوجل، فإن الحركة تُضفي معنىً. الحركة من العناصر الأساسيّة للتصميم الماديّ الجيّد. وبشكل عام، فإنك معتاد على وجود حركة في العالم الحقيقيّ. تساعدنا الحركة على معرفة كيف تعمل الأشياء وإلى أين علينا أن نركز انتباهنا. تستخدم التصاميم المادية نفس المفهوم، وتستخدم الحركة للتفاعل مع المستخدم، وتجعلك تعرف جيدًا كيف تستخدم التصميم. كيف تستخدم الحركة؟ ببساطة، أعطِ المستخدم تغذية راجعة عن الحدث الذي قام به. فمثلًا، هل ضغط المستخدم زرًّا؟ حرّكه لتظهر أن الأمر قد تمّ. مصدر الصورة: REDBUS APP – Material Design Preview - لـ Anish Chandran على Behance. 10. اجعل الحركات واقعيةكلمة "واقعية" هي مربط الفرس هنا. إن عصر الحركات المزيفة -حيث تتحرك الأشياء في داخل الشاشة- قد ولى منذ زمن. إذا كنت ستضمّن الحركة هذه الأيام، فيمكنك أيضًا أن تجعلها حقيقيّة بما يتوافق مع قوانين الفيزياء وكيفيّة عمل الأشياء في العالم الحقيقيّ. تخصص جوجل قسمًا كاملًا من توجيهات التصاميم المسطحة لمفهوم الحركة الواقعية. تشرح جوجل في هذه التوجيهات كيفية تقديم الكتلة والوزن، والتسارع والتباطؤ، وكيف يتم تحسين الحركة (بالإنجليزيّة: easing؛ وهي طريقة لجعل الحركة تبدو طبيعية أكثر بتغيير السرعة في أوقات محددة). التصاميم المادية جيدة فقط ما دامت تحاكي الحركة في الحياة الحقيقيّة. هذه هي الطريقة الوحيدة التي ستغني فيها الحركةُ الواجهةَ وتجعلها مفهومة أكثر للمستخدم. 11. اجعل كل شيء مستجيبًامن المبادئ الرئيسيّة للتصاميم الماديّة هي جعل العمل الناتج عملًا يمكن الوصول إليه واستخدامه على أيّ جهاز وأي حجم شاشة. وفوق كلّ شيء، الهدف هو جعل التجربة متّسقة. بهذه الطريقة، لن يتوه المستخدم إذا انتقل من جهاز إلى آخَر، حيث لا يجد واجهة مختلفة تمامًا بالنسبة له. باستخدام تصميم ماديّ جيّد، يمكن للمستخدم أن ينتقل دون أيّ عقبات، وأن يتابع في استخدام الموقع أو التطبيق من حيث تركه. ومن الطبيعي أن هذا يعني وجوب كون التصميم مستجيبًا. ولحسن الحظ، باستخدام الأطر البرمجية (frameworks) الحديثة، ستجد الكثير من الأشياء قد بُنيَت لك بالفعل، ولذا فجعل تطبيقك مستجيبًا لن يكون بتلك الصعوبة. 12. تذكر أن كل شيء في التفاصيلمن العناصر الأساسيّة التي تجعل التصاميم المسطحة صعبة التنفيذ جدًّا دون مشاكل هو أنها مبسطة للغاية. فمثلًا، في التصاميم المقلّدة للواقع الحقيقيّ (skeuomorphic) كانت التوجيهات بسيطة: اجعل كل جزء من التصميم يمثل قرينه من الحياة الحقيقيّة قدر الإمكان. وفيما يلي كيف أتت هذه الأشياء إلى الواقع: مصدر الصورة: 15 Puzzle - لـ Kamil Ginatulin على Behance. الأمور أبسط مع التصاميم الماديّة، ولكنها أعقد في الوقت نفسه. في غالب الأمر، التصاميم الماديّة هي لعبة التفاصيل. تحتاج فقط إلى القليل من الواقعية لتعبر عن الوظيفة الحقيقيّة والهدف من الأشياء التي تصممها، ولكنك في نفس الوقت لا ترغب بجعل الأشياء تبدو مطابقة تمامًا لمثيلاتها في الواقع الحقيقيّ. إن كنت في ريب، فتصفح بعض المعارض على الإنترنت لتوحي إليك ببعض الأفكار. هل لك تجارب مع التصاميم الماديّة بعد؟ هل تظن بالإمكان استخدامها لأكثر من مجرد تصميم تطبيقات أندرويد التي وُجِدَت أصلًا لأجله؟ شاركنا برأيك! ترجمة -وبتصرف- للمقال New to Material Design? 12 Principles You Need to Know لصاحبه Karol K.
×
×
  • أضف...