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



مزيد من الخيارات

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

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

نوع المُحتوى


التصنيفات

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

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • Node.js
    • jQuery
    • AngularJS
    • Cordova
  • HTML5
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • سي شارب #C
    • منصة Xamarin
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة Swift
  • لغة R
  • لغة TypeScript
  • سير العمل
    • Git
  • صناعة الألعاب
    • Unity3D
  • مقالات برمجة عامة

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

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

التصنيفات

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

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

  1. التدوين الصوتي هو طريقةٌ رائعةٌ لمشاركة المعلومات وبناء المجتمعات التي تتشارك بالاهتمامات، وهذا الدرس يمثِّل دليلًا لكيفية البدء بالتدوين الصوتي. السبب الرئيسي وراء قدرتي على إعطاء نصائح بالتدوين الصوتي هو أنني أدوِّن صوتيًا لما يقارب ثلاث سنوات. مدونتي الصوتية باسم Sysadministrivia تتضمن كلامًا عن إدارة الأنظمة. ما هو التدوين الصوتي؟ انتشر التدوين الصوتي انتشارًا كبيرًا في مختلف المجالات وأصبح شائعًا جدًا، إذ ينشر بعض الأشخاص تدوينات صوتية لمختلف جوانب حياتهم مثل جلسات الألعاب الإلكترونية التي يلعبونها. يعمل التدوين الصوتي بتسجيلك للصوت (أو الفيديو لأن البرمجيات قد تطورت حديثًا)، واستخدام صيغة بيانات قياسية لتنبيه المستمعين أنَّك نشرت التدوينة الصوتية podcast مباشرةً. صيغة RSS (المسؤولة عن إذاعة خبر نشر التدوينة) ومواصفة XML معينة (وهي صيغة البيانات القياسية) تجعل إنشاء هذه التنبيهات التلقائية أمرًا ممكنًا. كيف أستطيع البدء بالتدوين الصوتي؟ كل ما يلزمك هو حاسوب ونظام تشغيل. فمن البدهي أنَّك تحتاج إلى حاسوب، ولا يهم ما هو نظام تشغيلك، لكنني أنصحك باستخدام لينكس أو نظام من عائلة BSD. برمجيات تسجيل وتحرير الصوت يمكنك استخدام برمجيات متعددة المنصات (أي تعمل على أكثر من نظام تشغيل) لتسجيل الصوت مثل Audacity أو إذا كنتَ تفضِّل تحكمًا أكثر فانظر إلى Ardour. تحتوي صفحة hosts في موقع Sysadministrivia على قائمة بمواصفات العتاد التي نستعملها في التدوين (بما في ذلك الميكروفون والسماعات الرأسية والمعدات الأخرى)؛ لا أنصحك بشراء عتاد معيّن إذ هنالك مختلف أنواع الميكروفونات لتسجيل الكلام. سنحتاج إلى ميكروفون على شكل قلب (cardioid أو supercardioid) إذا كنت ستتحدث بالقرب من الميكروفون ، أو استخدام ميكروفون أحادي الاتجاه (omnidirectional) إذا أردتَ تسجيل ما يدور في غرفة مليئة بالأشخاص باستعمال ميكروفون وحيد، واحرص على أن تكون تلك الميكروفونات ذات مكثّف (condenser) لأنها أفضل لتسجيل الحديث، وسماعات الرأس ضرورية إذا كنت ستدون مع شخص آخر ليس في غرفتك نفسها. لمزيدٍ من المعلومات حول ذلك فأنصحك بقراءة قسم «Uploading» في التدوينة How to run your own podcast. استضافة الموقع ستحتاج إلى استضافة لتضع فيها الملفات الصوتية وملف XML الذي سيذيع خبر نشر التدوينات الجديدة، وموقع إلكتروني (ليس من الضروري امتلاك موقع، لكنني أنصحك بذلك بشدة). يمكنك بشكلٍ بديل أن ترفع الملفات الصوتية (أو الفيديو) إلى خدمة مثل YouTube أو SoundCloud واستخدام ميزة الاشتراك فيها لنشر خبر صدور صدور تدوينة جديدة؛ لكن دون استخدام RSS (أو XML) فهذا يعني أنَّ تدويناتك ليست تدوينات صوتية (podcast) تقنيًا، لأن هذه المواقع تطلب من مستخدميك أن يسجلوا حسابًا فيها، والتدوينات الصوتية يجب أن تسمح للمستمعين أن يبقوا مجهولين تمامًا. صيغة الملفات حسنًا، التدوينات الصوتية ليست «حرة» تمامًا، لاستخدام صيغة MPEG-1/2 Audio Layer III المعروفة باسمها المختصر MP3. هذه الصيغة محمية ببراءة اختراع ولا تتوافق مع مبادئ البرمجيات الحرة؛ لكن يمكن استخدام صيغة بديلة عنها وهي OGG لكنها ستجعل نشرك لها محدودًا، إلا أنَّ بإمكانك توفير تدوينة صوتية podcast و oggcast، وصيغة ملفات XML لهما متشابهة جدًا (لكن مع بعض الاختلافات التي يمكن تعلمها بسهولة). ملفات «التغذية» أفصل عادةً بين مختلف ملفات التغذية (feed files، التي تكون بصيغة XML)، لمختلف الخدمات مثل iTunes و Google Play والتدوينات الصوتية التقليدية (podcasts) و oggcasts. لاحظ أنَّ متصفح Firefox سيحاول تفسير (أو عرض) هذه الملفات باستخدام عميل RSS المضمّن فيه، ولعرض ملفات XML الفعلية فاستخدام الأمر curl أو wget مع الروابط السابقة، أو اعرض مصدر الصفحة في متصفحك. الطريقة السابقة تسمح لي باستخدام وسوم XML خاصة بكل خدمة، وللمزيد من التفاصيل التقنية راجع التدوينة How to run your own podcast. الموقع الإلكتروني أستخدمُ نظام إدارة محتوى باسم Textpattern مع بعض التعديلات، لكن الكثير من المدونين الصوتيين يستعملون ووردبريس مع إضافة PowerPress. كيف أوسِّع من جمهوري؟ الترويج Syndication أسهل طريقة لجلب مستمعين إلى تدويناتك الصوتية هي خدمات الترويج، فيوفر موقع DistroWatch ترويجًا لبعض تدوينات oggcast الصوتية. وأشهر الخدمات للترويج هي iTunes (لمزيدٍ من المعلومات راجع صفحة iTunes Connect Resources and Help page) و Google Play (زر صفحة Podcasts in Google Play Music page)، ويمكن أن تساعدك مواقع وخدمات الترويج الأصغر في زيادة متابعيك، لكن iTunes و Google Play هي أشهرها لسهولة الوصول إليها من مستخدمي iOS و أندرويد. وسائل التواصل الاجتماعي لا تنسَ التواصل المباشر على مواقع التواصل الاجتماعي، فيمكن أن تستعمل تويتر كطريقة أخرى لنشر تدويناتك الصوتية الحديثة، وسيسمح لك بالتواصل مباشرةً مع جمهورك. أمور متفرقة هنالك تقنيات أخرى لإشهار تدويناتك مثل بيع البضائع أو استضافة ضيوف في حلقاتك، والمشاركة في الأحداث المحلية والمنظمات الخيرية، لكن هذه التقنيات ستكون فعالة أكثر بعد أن يكون عندك جمهور. كيف أستفيد ماديًا من التدوين الصوتي؟ عليك أن تبدأ مشوارك في التدوين الصوتي لرغبتك في إنتاج ومشاركة محتوى مع فئة من الناس الذين يتشاركون الاهتمامات، فالإجابة باختصار هي: لن تتمكن من الاستفادة ماديًا (أو ربما تتمكن من ذلك إن كنتَ محظوظًا). لكن التدوين الصوتي في أغلبية الأوقات يكون نابعًا من رغبتك بمشاركة ما تعرفه مع الآخرين (كما في البرمجيات الحرة). ترجمة –وبتصرّف– للمقال A quick-start guide to podcasting لصاحبه Brent Saner
  2. هذا الدرس عبارة عن مدخل إلى SVG. سنتعلم فيه المعلومات الأولية التي تحتاجها لرسم أشكال SVG بسيطة باستخدام لغة XML الأساسية. فإن لم تستخدم SVG من قبل فهذا هو الدرس المناسب لك. في الماضي كان تنسيق الصور الوحيد المعتمد من قبل جميع المتصفحات هو تنسيق GIF وهو ملف صورة مطوّر من قبل CompuServe. ثم ظهرت ملفات صور JPEG التي تسمح بضغط الصور دون ضياع للتفاصيل مع حجم صغير مقارنة بملفات GIF. بعد فترة من الزمن ظهرت الحاجة إلى رسومات فكتور ثنائية الأبعاد على الإنترنت. وبعد دخول عدد من تنسيقات الصور إلى المنافسة في رابطة الويب W3C تم تطوير تنسيق SVG أخيرًا عام 1999. سوف أريكم الآن كيفية إنشاء أشكال باستخدام SVG. هذا الدرس سيشرح كيفية استخدام SVG في صفحات الويب. ما هو SVG؟ إن مصطلح SVG هو اختصار إلى Scalable Vector Graphics أي رسومات فكتور القابلة لتغيير الحجم وهو عبارة عن تنسيق صور قائم على لغة XML من أجل الويب. وعلى عكس تنسيقات GIF وPNG وJPEG فإن تنسيق SVG قابل لتعديل الحجم بسهولة دون أي ضياع للدقة ولتفاصيل الرسومات. إن ملف XML الذي يحوي صور SVG يمكن إنشاؤه وتخصيصه وتكامله مع بقية لغات W3C القياسية الأخرى مثل DOM وXSL باستخدام أي محرر نصوص. كما يمكن إنشاء صور SVG بصريًّا باستخدام برامج الرسم والفكتور كبرنامج أدوبي إليستريتور Adobe Illustrator. لماذا نستخدم SVG؟ يمكن إنشاؤها وتعديلها باستخدام أي محرر نصوص. يمكن طباعتها بدقة عالية جدًّا. يمكن استخدامها من أجل الرسومات المتحركة. موصى بها من قبل W3C. تعمل مع بقية لغات W3C القياسية مثل DOM. مظهرها عظيم للعروض البصرية. يمكن تعديل حجمها إلى أي حجم نريد بدون أي تشوه قد يطرأ عليها. SVG على صفحات HTML يمكن تضمين SVG بسهولة داخل ملفات HTML باستخدام وسم SVG. كما في أسطر الأوامر التالية. <svg width=" " height=" "> [element code here..] </svg> كماترى فإن SVG لها وسمها الخاص. يجب أن يتم تحديد طول وعرض الرسم وذلك لاحتواء عنصر الرسم. العناصر التالية يمكن أن تستخدم للرسم داخل مساحة العمل الخاصة. Circle Rectangle Ellipse Line Polyline Polygon Path Text فلنبدأ الآن بتعلم استخدام هذه العناصر في الرسم. إنشاء الدائرة في SVG دوائر الـ SVG يمكن تنفيذها باستخدام وسم circle. كهذا المثال. <svg height="300" width="300"> <circle cx="60" cy="60" r="50" style="fill: blue; stroke: black; stroke-width: 2px;" /> </svg> النتيجة هي صورة الدائرة الظاهرة في الصورة التالية. في هذا المثال استخدمت وسم circle ثم cx (إحداثيات x) وcy (إحداثيات y) والتي تحدد مركز الدائرة المرسومة. ثم وضعت القيمة 50 للمتغير r (نصف القطر) والذي سيحدد طول الخط الواصل بين مركز الدائرة ومحيطها. أخيرًا أضفت أنماط للون التعبئة والحدود وعرض الحدود. إنشاء مستطيل في SVG يمكن تنفيذ رسم المستطيل عبر وسم rectangle ويمكن تحديد قيم مختلفة من الارتفاع والعرض. <svg height="300" width="300"> <rect width="250" height="100" rx="11" ry="11" style="fill: yellow; stroke: green; stroke-width: 4px;"/> </svg> النتيجة النهائية هي صورة SVG عبارة عن مستطيل كما في الصورة التالية. قمتُ بتحديد العرض والارتفاع باستخدام وسم rectangle. ثم أضفت rx وry التي تحدد استدارة حدود المستطيل. إذا قمت بإزالة القيمتين الأخيرتين فستحصل على حواف حادة. ثم نضيف أنماطًا عبر لون التعبئة والحدود وعرض الحدود. إنشاء القطع الناقص (شكل بيضوي) في SVG يتم تنفيذ هذا الشكل عبر وسم ellipse. القطع الناقص ليس له ارتفاع وعرض متساويين وعلى عكس الدائرة فإن نصف القطر cx وcy مختلفان. انظر الكود البرمجي التالي. <svg height="300" width="300"> <ellipse cx="190" cy="70" rx="100" ry="50" style="fill:red; stroke:green;stroke-width:2"/> </svg> صورة القطع الناقص (الشكل البيضوي) النهائية كما في الصورة التالية. إن cx وcy يحددان مركز شكل القطع الناقص بينما rx وry يحددان قطري الشكل. وكما ترى فإن rx يحدد عرض الشكل بينما ry يحدد ارتفاع الشكل. ثم نضع الأنماط من ألون التعبئة والحدود وعرضها كما في كل مرة. إنشاء خط في SVG يمكن إنشاء خط SVG باستخدام وسم line. كما هو واضح من الاسم فإن هذا الوسم يرسم خطوطًا، كما في المثال التالي. <svg height="300" width="300"> <line x1="0" y1="0" x2="100" y2="150" style="stroke:#000; stroke-width:5" /> </svg> النتيجة النهائية ستظهر كما في الصورة التالية. في هذا المثال يمثّل x1 الاحداثي x فيما يمثّل y1 الاحداثي y وهو ما يحدد نقطة بداية الخط. فيما تحدد الاحداثيات x2 وy2 نقطة النهاية. وباستخدام ميزات الانماط وضعتُ لون الحدود أسود ولعرض 5بكسل. إنشاء الخطوط المتعددة في SVG يمكن تنفيذ هذه الخطوط عبر وسم polyline. حيث يستخدم لرسم الأشكال المكونة من خطوط مستقيمة كهذا المثال. <svg height="300" width="300"> <polyline points="60,50 150,120 100,220 200,170" style="fill:none;stroke:black;stroke-width:3" /> </svg> والنتيجة كما هي واضحة في الصورة. وباستخدام الاحداثيات x وy يمكنك تعيين كل نقطةمن النقاط الفردية لإنشاء أي شكل تريده. وكما ترى هنا لدي أربع نقاط تم وصلها ببعضها لتشكيل الخطوط. وأضفت حدودًا سوداء وبعرض 3 بكسل. إنشاء شكل مضلع في SVG يمكن تنفيذه عبر وسم polygon. هذا العنصر سيرسم شكلًا مكونًا من أكثر من ثلاثة جوانب. شاهد الترميز التالي. <svg height="300" width="300"> <polygon points=" 60,20 100,40 100,80 60,100 20,80 20,40" style="fill:cyan;stroke:red;stroke-width:1" /> </svg> النتيجة النهائية كما في الصورة. في هذا الشكل نقاط يتم تحديدها عبر الاحداثيات x وy لكل جانب من جوانب الشكل من النقطة الأولى إلى النقطة الأخيرة. في المثال، أنشأتُ شكلًا مسدّسًا بست جوانب. كما ترى هناك 6 نقاط متصلة ببعضها محددة بالاحداثيات x وy. ثم حددت لون التعبئة باللون السماوي مع لون حدود أحمر وبعرض 1 بكسل. إنشاء مسارات في SVG يمكن تنفيذه عبر وسم path. هذا العنصر سيرسم مسارًا مخصّصًا وأشكالًا تتكون من منحنيات، خطوط وأقواس. من بين جميع عناصر SVG هذا هو العنصر الأصعب للتعلم. مسارات SVG تستخدم الأوامر التالية. M للحركة L للخط V للخط العمودي H للخط الأفقي C للمنحني S للمنحني الناعم T لمنحنيات البيزير التربيعية A للأقواس البيضوية Z لإغلاق المسار وتحدد الأحرف الكبيرة الموقع بدقة بينما تحدد الأحرف الصغيرة الموقع بشكل تقريبي. كما في المثال التالي. <svg height="300" width="300"> <path d="M 30 40 C 140 -30 180 90 20 160 L 120 160" style="fill: none; stroke: black; stroke-width: 6px;" /> </svg> النتيجة ستكون كالصورة التالية من خلال الترميز في الأعلى سترى بأنني استخدمت حرف d. ميزة حرف d هذا ستكون دائمًا أمر التحريك. ثم استخدمت حرف M الذي يعني التحرك نحو اتجاه معين. وقبل رسم أي شيء عليك أن تحرك المؤشر الافتراضي إلى الموقع المفضّل. في هذا المثال حرّكتُ المحور x إلى 30 والمحور y إلى 40. المنحني يبدأ عند 140,-30 كنقطة للبداية. تاليًا نزلت نقاط المنحني للأسفل واليمين عند النقطة 180,90 وتنتهي عند 20,160 ولإنهاء المسار أنشأت خطًّا عند النقطة 120,160. إنشاء النص في SVG يمكن تنفيذ هذا الأمر من خلال الوسم المغلق text. هذا العنصر يستخدم لرسم النص في صورة SVG. <svg height="300" width="300"> <text x="20" y="30" fill="blue" font-size="20">This a great sample for Text SVG! </text> </svg> النتيجة ستكون كالتالي. في المثال استخدمت موقع المحور x للنص عند 20. هذا سيضع النص على بعد 20 بكسل من اليسار بينما وضعت المحور y عند 30 ما يجعل النص على بعد 30 بكسل عن الحافة العلوية. ثم لوّنت النص باللون الأزرق وجعلت حجمه عند 20 بكسل. هذا سيظهر العبارة التالية "This a great sample for Text SVG!". ترجمة -وبتصرف- للمقال How to Create Simple Shapes with SVG لصاحبه Editorial Team
  3. سنتناول في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms تطبيقًا عمليًا آخرًا وهو تطبيق قارئ الخلاصات من موقع أكاديمية حسوب. تندرج مثل هذه التطبيقات تحت النوع rss feed reader ولها مزايا كثيرة ومتنوّعة. سيكون تطبيقنا بسيطًا وعمليًّا ويوضّح مبدأ العمل كما جرت العادة. سننفّذ التطبيق على مرحلتين: المرحلة الأولى سيكون تطبيق لعرض الخلاصات الموجودة ضمن أحد أقسام الموقع، مع إمكانية اختيار أيّ خلاصة وعرض تفاصيل حولها، وهذا هو محتوى هذا الدرس. أمّا المرحلة الثانية فستكون تحسين للتطبيق المنجز في هذا الدرس حيث سنضيف إمكانيّة قراءة الخلاصات من جميع أقسام الموقع مع إضافة بعض التحسينات على أسلوب العرض بشكل عام، وسيكون ذلك في درس منفصل. ماهي الخلاصات؟ خلاصات موقع تكون عادةً عبارة عن مستند XML يحتوي على بيانات تمثّل آخر الأخبار التي يوفّرها الموقع. لا تمتلك جميع المواقع خلاصات بالطبع، ولكنّ من الجيّد دومًا أن يوفّر الموقع مثل هذه الخلاصات في حال كان يمتلك محتوىً متجدّدًا كما هو الحال في موقع أكاديميّة حسوب. بما أنّ الخلاصة تكون ضمن ملف XML فمن البديهي أن تكون البيانات منسّقة بتنسيق XML، أي على شكل عقد آباء وأبناء. انظر مثلًا إلى جزء من خلاصات قسم مقالات البرمجة في أكاديميّة حسوب كما ظهرت لي عند كتابة هذا المقال: لاحظ أنّني أستعرض هذه الخلاصات عن طريق متصفّح Chrome عن طريق الرابط: https://academy.hsoub.com/programming/?rss=1 وهو الرابط الذي توفّره الأكاديميّة للحصول على خلاصات مقالات البرمجة. من الواضح أنّ هذا الأسلوب غير عملي في الحصول على آخر الخلاصات، لذلك يلجأ المستخدمون عادةً إلى تطبيقات متنوّعة لعرض هذه الخلاصات بشكل مريح ومقروء. تطبيق قارئ الخلاصات سنبني في هذا الدرس تطبيق قارئ خلاصات وظيفته قراءة الخلاصات الموجودة في قسم مقالات البرمجة في موقع أكاديميّة حسّوب. وسنعمل في الدرس التالي على تحسين هذا التطبيق بإتاحة الإمكانيّة لتصفّح الخلاصات من جميع الأقسام. أنشئ مشروعًا جديدًا من النوع Blank App (Xamarin.Forms Portable) وسمّه BasicFeedReader ثم أبق فقط على المشروعين BasicFeedReader (Portable) و BasicFeedReader.Droid كما وسبق أن فعلنا في هذا الدرس. الصنف FeedItem من نافذة مستكشف الحل Solution Explorer انقر بزر الفأرة الأيمن على المشروع BasicFeedReader واختر من القائمة التي ستظهر الخيار Add ثم من القائمة الفرعية الخيار New Folder لإضافة مجلّد جديد. سمّ هذا المجلّد بالاسم Entities، وبعد أن يظهر في نافذة الحل Solution Explore انقر عليه بزر الفأرة الأيمن واختر الخيار Add ومن القائمة الفرعية اختر Class. ستظهر نافذة تسمح لك بتعيين اسم لهذا الصنف. اختر الاسم FeedItem له. هذا الصنف هو حجر البناء الأساسي لهذا البرنامج. احرص على جعل محتويات الملف FeedItem.cs كما يلي: namespace BasicFeedReader.Entities { public class FeedItem { public string Link { get; set; } public string Title { get; set; } public string Description { get; set; } public override string ToString() { return Title; } } } الخصائص Link و Title و Description في هذا الصنف تقابل العناصر link و title و description في مستند XML. أمّا التابع ToString فهو للحصول على تمثيل نصّي لأي كائن من النوع FeedItem. الواجهات أضف مجلّدًا جديدًا للمشروع BasicFeedReader كما فعلنا قبل قليل، وسمّه Pages. ثم أضف إليه صفحتي محتوى تعتمدان رماز XAML بحيث يكون اسم الصفحة الأولى هو FeedDetailsPage وهي الواجهة الرئيسيّة، واسم الصفحة الثانية SectionFeedsPage وهي واجهة التفاصيل. الصفحة SectionFeedsPage هي الواجهة التي ستعرض خلاصات قسم مقالات البرمجة في أكاديمّية حسوب. أمّا الواجهة FeedDetailsPage فهي لعرض الخلاصة التي يتم اختيارها من الواجهة SectionFeedsPage. الواجهة SectionFeedsPage انتقل إلى الملف SectionFeedsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="BasicFeedReader.Pages.SectionFeedsPage"> <StackLayout> <ListView x:Name="lsvFeeds" ItemTapped="lsvFeeds_ItemTapped"/> </StackLayout> </ContentPage> لاحظ كم هو بسيط هذا الرماز، فهو لا يحتوي سوى عنصر قائمة lsvFeeds لعرض خلاصات قسم مقالات البرمجة، حيث سنربط الحدث ItemTapped الخاص بها بالمعالج lsvFeeds_ItemTapped. انتقل الآن إلى ملف الشيفرة البرمجيّة الموافق SectionFeedsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using BasicFeedReader.Entities; using System.Linq; using System.Xml.Linq; using Xamarin.Forms; namespace BasicFeedReader.Pages { public partial class SectionFeedsPage : ContentPage { public SectionFeedsPage() { InitializeComponent(); LoadFeeds("https://academy.hsoub.com/programming/?rss=1"); Title = "مقالات البرمجة"; } private async void lsvFeeds_ItemTapped(object sender, ItemTappedEventArgs e) { FeedItem selectedFeed = (FeedItem)e.Item; FeedDetailsPage feedDetailsPage = new FeedDetailsPage(selectedFeed); await Navigation.PushAsync(feedDetailsPage); } private void LoadFeeds(string resource) { XDocument doc = XDocument.Load(resource); var feeds = from newsItem in doc.Descendants("item") select new FeedItem { Title = newsItem.Element("title").Value, Description = newsItem.Element("description").Value, Link = newsItem.Element("link").Value }; lsvFeeds.ItemsSource = feeds; } } } عند يتم إنشاء كائن من هذه الصفحة لعرضها للمستخدم، يتم تنفيذ بانيتها. حيث يعمل التطبيق على استدعاء التابع LoadFeeds والذي يتطلّب وسيطًا واحدًا من النوع string ويمثّل عنوان المصدر المزوّد للخلاصات. في الحقيقة ما فعلناه هنا هو أمر غير جيّد من الناحية العمليّة، فلا ينبغي وضع مثل هذا الاستدعاء هنا في البانية، سيما وأنّ التابع LoadFeeds لا يستخدم تقنية البرمجة غير المتزامنة، مما سيسبب جمودًا مزعجًا في التطبيق عند أوّل تشغيله. سنحل هذه المشكلة لاحقًا في الجزء الثاني. انظر الآن إلى تعريف التابع LoadFeeds من الشيفرة السابقة. ستلاحظ أنّه يستخدم تقنيّة ممتازة يوفرها إطار العمل .NET من خلال الصنف XDocument الذي يمثّل مستند XML بشكل كائني، حيث يعمل السطر الأوّل من هذا التابع على إنشاء كائن جديد من الصنف XDocument من خلال تحميل مستند XML مباشرةً من الانترنت عن طريق التابع الساكن Load. بعد ذلك يتم إعراب مستند XML المحمّل من الإنترنت بسرعة وفعاليّة عاليتين. حيث تحتاج إلى عدد قليل من الأسطر البرمجيّة على شكل استعلام LINQ to XML لكي تقوم بالمطلوب. لقد اطلعنا على تقنيّة شبيهة في درس سابق حيث استخدمنا LINQ to Objects. يمكنك معرفة المزيد حول هذا الموضوع بقراءة هذا المقال. بعد استخلاص المعلومات من مستند XML المُحمّل، وإنشاء كائن من النوع FeedItem لكل خلاصة موجودة في هذا المستند، يتم إسناد المجموعة المنشأة من هذه الكائنات إلى الخاصيّة ItemsSource من القائمة lsvFeeds ليتم عرضها للمستخدم. أمّا بالنسبة لمعالج الحدث lsvFeeds_ItemTapped من الشيفرة السابقة. فوظيفته بسيطة، وهي تنحصر في الحصول على كائن FeedsItem الموجود ضمن العنصر الذي تمّ نقره (لمسه) ضمن القائمة lsvFeeds ومن ثمّ الانتقال إلى الصفحة FeedDetailsPage لعرض محتوى هذه الخلاصة. الواجهة FeedDetailsPage انتقل إلى الملف FeedDetailsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="BasicFeedReader.Pages.FeedDetailsPage"> <StackLayout Orientation="Vertical"> <WebView x:Name="wvDescription" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"/> </StackLayout> </ContentPage> لاحظ مرّة أخرى كم هو بسيط هذا الرماز. حيث يقتصر على استخدام عنصر واحد، هو العنصر WebView ويُستخدم لعرض محتوى مستند HTML. وسبب استخدامي لهذا العنصر، هو أنّ الخلاصات التي ترد من موقع أكاديميّة حسّوب تحتوي على المحتوى كاملًا وهو منسّق بتنسيق HTML. وهذا أمر لا تجده في كثير من مزوّدات خدمة الخلاصات. لذلك فوجدت أنّ أفضل طريقة هو عرض هذا المحتوى مباشرةً ضمن عنصر WebView. انتقل الآن إلى ملف الشيفرة البرمجيّة الموافق FeedDetailsPage.xaml.cs واحرص على أن يكون كما في الشكل التالي: using Xamarin.Forms; using BasicFeedReader.Entities; namespace BasicFeedReader.Pages { public partial class FeedDetailsPage : ContentPage { public FeedDetailsPage(FeedItem feedItem) { InitializeComponent(); this.Title = feedItem.Title; var descriptionHtmlSource = new HtmlWebViewSource(); descriptionHtmlSource.Html = @"<html dir='rtl'><body>" + feedItem.Description + "</body></html>"; wvDescription.Source = descriptionHtmlSource; } } } كل الشيفرة البرمجيّة موجودة ضمن البانية التي تتطلّب وسيطًا واحدًا من النوع FeedItem الذي يحتوي على بيانات الخلاصة المراد عرض تفاصيلها. نعمل على وضع عنوان هذه الصفحة ليكون مطابقًا لعنوان الخلاصة، ثمّ ننشئ كائنًا من النوع HtmlWebViewSource نسنده ضمن المتغيّر descriptionHtmlSource الذي سيمثّل الكائن المحتوى للعنصر wvDescription (عنصر WebView الذي صرّحنا ضمن ملف الرماز). لاحظ كيف أسندنا للخاصيّة Html لهذا المتغيّر محتوى الخاصيّة Description لكائن الخلاصة، وهو كما أشرنا قبل قليل عبارة عن مستند HTML ينقصه فقط الوسمين و اللذان أضفناهما يدويًّا. ثمّ نُسند المتغيّر descriptionHtmlSource بدوره إلى الخاصيّة Source للعنصر wvDescription مما يؤدّي إلى ظهور مستند HTML كما هو مخطّط له. ملف التطبيق App.cs انتقل إلى ملف التطبيق App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = MainPage = new NavigationPage(new SectionFeedsPage()); } احرص على استخدام فضاء الاسم Pages لكي تستطيع الوصول إلى الصفحة SectionFeedsPage كما يلي: using BasicFeedReader.Pages; ستضع السطر السابق أوّل الملف App.cs كما هو معلوم. نفّذ البرنامج، ستحصل على شكل شبيه بما يلي: وهي الواجهة التي تحتوي على الخلاصات الموجودة ضمن قسم مقالات البرمجة. جرّب اختيار أحد هذه الخلاصات لتحصل على شكل شبيه بما يلي: لاحظ كيف تظهر الخلاصة كما لو أنّه يتم عرضها ضمن متصفّح الويب العادي. جرّب سحب الصفحة إلى الأسفل ليؤكّد ذلك هذه الملاحظة. الخلاصة تناولنا في هذا الدرس الجزء الأوّل من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسّوب. حيث نفّذنا بعض المهام الأساسيّة، والتي سنبني عليها في الجزء الثاني الذي سيتناول تطبيقًا محسّنًا لهذا التطبيق. حيث سنعمل على دعم عرض جميع الأقسام المتاحة في الأكاديميّة وليس قسم مقالات البرمجة فحسب.
  4. تنتشر الهواتف المحمولة والحواسيب اللوحية العاملة بنظام تشغيل أندرويد في أحجام مختلفة ودقة شاشات مختلفة، لذلك من الأفضل دائمًا إذا استطاع التطبيق أن يتأقلم مع حجم ودقة الشاشة لتقديم أفضل تجربة استخدام مع الاستغلال الأمثل لشاشة. ويمكن أن تبني تطبيقًا تتغير الواجهة الخاصة به بشكل ديناميكي تبعًا للمساحة المتاحة في الشاشة، فإذا لم تتواجد مساحة كافية، يظهر جزء من الواجهة فقط في الشاشة ومنه يمكن الانتقال للواجهة الأخرى مثل الصورة التالية: أما إذا توافرت المساحة الكافية فيمكن دمج أكثر من واجهة في واجهة واحدة لسهولة التنقل بينها كما في الصورة التالية: لذا يوفر أندرويد عدة طرق للقيام بذلك من ضمنها استخدام الـ 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; } } بعد ذلك نقوم بتشغيل التطبيق على المحاكي وتجربته للتأكد من أداءه للوظيفة المطلوبة بشكل سليم. بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  5. يتميز نظام تشغيل أندرويد بوجود شريط أعلى الشاشة يمُد المستخدم بالعديد من المعلومات الهامة من ضمنها الإشعارات التي ترسلها التطبيقات. فالإشعارات هي جزء من واجهة المستخدم لكنها تظهر خارج التطبيق لإعلام المستخدم بحدث معين، مما يُمكّن المستخدم من عرضه والتفاعل معه بينما يستخدم تطبيقًا آخر. ويحتوي الإشعار على رسالة مختصرة عن هذا الحدث. ويظهر الإشعار عند حدوثه كأيقونة تعبر عن التطبيق في شريط الحالة أعلى الشاشة، ويمكنك معرفة تفاصيله عند سحب درج الإشعارات والضغط على الإشعار. هناك عدة خطوات لصنع الإشعارات من داخل تطبيقك: صنع كائن البناء الخاص بالإشعار من الصنف 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> بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  6. لدى برمجة تطبيقات أندرويد، فإنّه وفي العديد من الأحيان نحتاج أن نعرض مجموعة من العناصر معًا في النشاط أمام المستخدم كعرض جهات الاتصال مثلًا أو الرسائل حيث يمكن للمستخدم أن يتصفحها ويتنقل بينها سريعًا، كما يمكنه الضغط على أي منها فيتم عرض المزيد من المعلومات عن هذا العنصر. نستخدم عنصر الواجهة 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(); } }; } } الآن قم بتجربة التطبيق على المحاكي للتأكد انه يعمل كما ينبغي. بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  7. العناصر الأساسية في تطبيقات أندرويد تتكون تطبيقات أندرويد من أربعة عناصر أساسية لكل منها دورها الخاص الذي تستطيع القيام به، ويتم استخدامها بناءً على الغرض أو المهمة المطلوب تنفيذها وهي: النشاطات 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"); } } } ثم نقوم بتجربة التطبيق على المحاكي. بهذا نكون قد وصلنا إلى نهاية هذا الدرس، في انتظار تجربتكم وآرائكم.
  8. بعد أن أنشأنا أول مشروع في 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" بهذا نكون قد وصلنا إلى نهاية ثاني دروسنا من سلسلة أندرويد للمبتدئين، في انتظار تجربتكم وآرائكم.
  9. أنا منبهر كثيرًا بجميع نواحي المحادثة ذات 23 عامًا، التي أدت إلى إنشاء عنصر HTML الذي استعمل في كل صفحة ويب تقريبًا. خذ بعين الاعتبار أنَّ: HTTP ما زال موجودًا، ونجح بالتطور من الإصدار 0.9 إلى 1.0 ثم إلى 1.1، وما يزال يتطور. HTML ما زالت موجودةً، صيغة البيانات البدائية تلك التي لم تكن تدعم تضمين الصور! تطورت إلى الإصدار 2.0 ثم إلى 3.2 ثم إلى 4.0، أي أنَّ خط تطويرها لم ينقطع، لكنه بالتأكيد خطٌ ملتوٍ ومتعرج، وفيه العديد من النهايات المغلقة. لكن ها نحن ذا في عام 2016، وما زالت صفحات الويب من عام 1990 تُعرَض بشكلٍ جيد في المتصفحات الحديثة. ولقد فتحتُ إحداها في متصفح هاتفي الحديث العامل بنظام أندرويد، ولم أحصل على رسالة تقول "الرجاء الانتظار إلى أن يتم استيراد الصيغة القديمة". نتجت HTML من النقاشات بين صانعي المتصفحات والمطورين وواضعي المعايير والأشخاص الذين يحبون التحدث عنها. أغلبية الإصدارات الناجحة من HTML كانت عبارة عن إضفاءِ صفةٍ رسميةٍ لما هو موجودٌ مُحاوِلَةً توجيهه نحو الطريق الصحيح. أي شخص يخبرك أنه يجب أن تكون HTML «صرفة» (أي على الأرجح بتجاهل صانعي المتصفحات أو المطورين أو كليهما) هو شخصٌ لديه معلوماتٌ خاطئة. لم تكن HTML صرفةً أبدًا، وأيةّ محاولات لجعلها كذلك قد باءت بالفشل. لم يبق أي متصفح منذ عام 1993 موجودًا كما هو. فتم التخلي عن متصفح Netscape Navigator في 1998، ثم أعيدت كتابته من الصفر لإنشاء Mozilla Suite، ثم تم الاشتقاق لإنشاء Firefox. وكانت لمتصفح Internet Explorer بداياتٌ متواضعة في «Microsoft Plus!‎» لويندوز 95، حيث حُزِّمَ مع بعض سمات سطح المكتب ولعبة pinball (لكن بالطبع يمكن تتبع تاريخ المتصفح إلى أبعد من تلك النقطة). بعض أنظمة التشغيل من عام 1993 ما زالت موجودةٌ، لكن ليس لها صلة بالويب الحديث الذي نعرفه. فأغلبية مستخدمي الويب الآن بدؤوا باستعماله على حاسوب مكتبي يعمل بنظام ويندوز 2000 أو أحدث منه، أو على جهاز Mac يعمل بنظام OS X، أو على حاسوب مكتبي يعمل بتوزيعة من توزيعات لينُكس، أو على هاتف محمول مثل هواتف أندرويد أو iPhone. لكن كان إصدار ويندوز 3.1 في عام 1993، وكان لينُكس يوزَّع عبر Usernet. بقي بعض الأشخاص موجودين وما يزالون منهمكين في العمل على ما ندعون «معايير الويب»، حيث بقي هؤلاء الأشخاص يعملون لأكثر من 20 عامًا، وعمل بعضهم على اللغات التي تسبق HTML التي يمكن تتبع تاريخها إلى ثمانينيات القرن الماضي. بالحديث عن اللغات التي سبقت HTML، أصبح من السهل نسيان الصيغ والأنظمة التي كانت موجودةٌ عند إنشاء HTML بعد الانتشار الواسع والكبير للغة HTML والويب. ماذا عن Andrew؟ أو Intermedia؟ أو HyTime؟ لم تكن HyTime مشروعًا بحثيًا أكاديميًا لا شأن لها، وإنما كانت من معايير ISO، ولقد كانت تُستعمل لأغراضٍ عسكرية. لم يُجِب كل ما سبق عن تساؤلنا الأصلي: لماذا لدينا عنصر <img>؟ لماذا ليس عنصر <icon>؟ أو عنصر <include>؟ لماذا ليس رابطًا مع خاصية include، أو مجموعةٌ من قيم الخاصية rel؟ لماذا عنصر <img>؟ الجواب بسيطٌ للغاية، لأن Marc Andreessen قام بتنفيذ (بناء) وتوفير العنصر <img> في مُتصفّحه والذي يقوم ببناء العنصر وتوفيره في متصفّحه سيربح. لكن هذا لا يعني أنَّ مَن يبني العُنصر وينشره سيربح دائمًا، فلا تنسَ أنَّ Adnrew و Intermedia و HyTime قامت بذلك أيضًا، فبناء العنصر وتوفيره هو شرطٌ لازمٌ لكنه غير كافٍ للنجاح؛ وأنا هنا لا أقصد أنَّ بناء العُنصر قبل المعيار هو الحل الأمثل، ووسم <img> لم يكن يستعمل صيغة صور شائعة، ولم يُعرِّف كيف يلتف النص حول الصورة، ولم يكن يدعم النص البديل أو محتوى احتياطي للمتصفحات القديمة، وبعد 23 عامًا، ما زلنا نعاني مع مشكلة Content Sniffing، وما تزال هذه المشكلة مصدرًا للثغرات الأمنية. ويمكنك تتبع كل هذا إلى 23 عامًا مضت، مرورًا بحرب المتصفحات، إلى 23 شباط/فبراير عام 1993، عندما قال Marc Andreessen بشكلٍ عابر «ربما في يوم ما سنعتمد الـ MIME» ومع ذلك نشر العنصر الذي قام ببنائه وتضمينه في مُتصفّحه. التسلسل الزمني لتطوير HTML من 1997 إلى 2004 في كانون الأول/ديسمبر من عام 1997، نشرت رابطة الشبكة العالمية (W3C) نسخة HTML 4.0 ثم أغلقت مجموعة عمل HTML‏ (HTML Working Group) في الحال. وبعد أقل من شهرين، نشرت مجموعة عمل أخرى من W3C معيار XML 1.0، وبعد ذلك بثلاثة أشهر، أنشَأ القائمون على W3C ورشة عمل (workshop) اسمها "بلورة مستقبل HTML"‏ (Shaping the Future of HTML) للإجابة عن هذا السؤال: "هل تخلت W3C عن HTML؟" وكان جوابهم: منحت W3C مجموعة عمل HTML هذا التكليف لإنشاء "مجموعة من وسوم XML"، كانت أول خطوة –في كانون الأول/ديسمبر 1998– هي كتابة مسودة لمعيار مؤقت (انتقالي) الذي كانت مهمته إعادة قولبة HTML في XML دون إضافة أيّة عناصر أو خاصيات جديدة، عُرِفَ هذا المعيار لاحقًا باسم "XHTML 1.0" وعرَّف نوع MIME جديد لمستندات XHTML ‏"application/xhtml+xml"، لكن لتسهيل انتقال صفحات HTML 4 الموجودة من قبل، فقد تضمن المعيار "الملحق C" الذي "يلخص إرشادات التصميم للمطورين الذين يريدون تشغيل مستندات XHTML في متصفحات HTML الموجودة من قبل"، إذ قال الملحق C أنك تستطيع كتابة ما يسمى "صفحات XHTML" لكن تستطيع تخديمها عبر نوع MIME القديم text/html. الهدف الجديد هو النماذج (forms)، ففي آب/أغسطس 1999، نشرت مجموعة عمل HTML نفسها أول مسودةٍ لنماذج XHTML الموسعة ولقد وضعوا التوقعات التي يرمون إلى تنفيذها في أول فقرة: وبعد عدِّة أشهر أُعيدت تسمية "XHTML Extended Forms" إلى "XForms" وانتقلت إلى مجموعة العمل الخاصة بها، وعَمِلَت هذه المجموعة على التوازي مع مجموعة عمل HTML ونَشرت في النهاية الإصدار الأول من XForms 1.0 في تشرين الأول/أكتوبر 2003. وفي تلك الأثناء -وبعد اكتمال التحويل إلى XML- حطَّت مجموعة عمل HTML أنظارها إلى إنشاء "الجيل الجديد من HTML"، ففي أيار/مايو 2001، نشروا الإصدار الأول من XHTML 1.1، الذي أضاف بعض الميزات الصغيرة على XHTML 1.0، لكنه أزال الملحق C، فيجب -بدءًا من الإصدار 1.1- تخديم مستندات XHTML بنوع MIME الخاص بها application/xhtml+xml. كل شيء تعرفه عن XHTML خاطئ لماذا أنواع MIME مهمة جدًا؟ لماذا أذكرها بين الحين والآخر؟ لسبب واحد: draconian error handling (أي عدم التساهل في معالجة الأخطاء). فكانت المتصفحات "متسامحة" مع HTML، فلو أنشأت صفحة HTML ولكن نسيت وسم الإغلاق ‎</head>‎، فستُظهِرها المتصفحات على أيّة حال (لأن هنالك وسومًا معيّنة تشير إلى نهاية قسم <head> وبداية <body>)، فيجب عليك أن تضع هيكلية معيّنة عند تداخل الوسوم (أي إغلاق الوسوم المفتوحة بنمط "last-in-first-out" أي أن آخر وسم مستعمل سيُغلق أولًا) لكن إن كتبتَ <b><i></b></i>، فستتعامل المتصفحات معها (بطريقةٍ ما) وستكمل عرض الصفحة دون إظهار رسالة خطأ. وكما قد تتوقع، أدت عدم كتابة شيفرات HTML بطريقة سليمة تمامًا إلى جعل المطورين يكتبون شيفرات غير سليمة. ففي بعض التقديرات، أكثر من 99% من صفحات HTML على الويب فيها خطأ واحد على الأقل، لكن لما كانت هذه الأخطاء لا تسبب عرض رسالة خطأ مرئية من المتصفحات، ولهذا لن يصلحها أحد. رأت W3C هذه المشكلة الأصولية في الويب، ثم همّوا لتصحيحها. لغة XML، المنشورة في 1997، تخلصت من العملاء المتساهلين وقالت بأن جميع البرامج التي تستعمل XML يجب أن تعامل الأخطاء المتعلقة "بطريقة الكتابة السليمة" على أنها أخطاء (Fetal Errors). اشتهر مفهوم توقف المعالجة عند أول خطأ بالاسم "draconian error handling" وذلك نسبةً إلى القائد الإغريقي Draco الذي شرَّع عقوبة الموت لأيّة تجاوزات (ولو صغيرة نسبيًا) لقوانينه. فعندما أعادت W3C صياغة HTML بمفردات XML، اعتبرت أنَّ جميع المستندات التي تُخدَّم بنوع MIME الجديد application/xhtml+xml ستخضع إلى سياسة draconian error handling، فلو كان هنالك خطأٌ ما في أسلوب صياغة صفحة XHTML -نسيان وسم الإغلاق ‎</head>‎ أو تداخل غير صحيح للوسوم على سبيل المثال- فلن يكون لمتصفح الويب خيارٌ إلا إيقاف معالجة الصفحة وإظهار رسالة خطأ إلى المستخدم النهائي. لم تكن هذه الفكرة شائعةً على نطاقٍ واسع، لأن النسبة المُقدَّرة للأخطاء في صفحات الويب هي 99%، هذا يعني أن رسائل الخطأ ستُعرَض على المستخدم طوال الوقت، ولقلة الميزات التي توفرها XHTML 1.0 و 1.1، فقرر مطورو الويب تجاهل application/xhtml+xml، لكن هذا لا يعني أنهم تجاهلوا XHTML بالكلية، لأن الملحق C من معيار XHTML 1.0 أعطى مطوري الويب ثغرةً يمكنهم استعمالها: "اكتب شيئًا يشبه بنية XHTML، لكن خدِّمه بنوع MIME القديم text/html"، وهذا ما فعله الآلاف من مطوري الويب، إذ «حدَّثوا» صفحاتهم إلى بنية XHTML لكن بقيت تلك الصفحات مُخدَّمة بنوع MIME القديم text/html. ولليوم، ما تزال تعلن ملايين الصفحات أنها XHTML، فيبدؤون بنوع المستند doctype الخاص بلغة XHTML في أول سطر، ويستعملون أحرفًا صغيرةً لأسماء الوسوم، ويستعملون علامات الاقتباس حول قيم الخاصيات، ويضيفون خطًا مائلًا بعد العناصر الفارغة مثل <br /‎> و <hr /‎> لكن قلّة قليلة من تلك الصفحات تُخدَّم بنوع MIME الجديد application/xhtml+xml، الذي سيُفعِّل عدم التساهل في الأخطاء الخاص بلغة XML. فأيّة صفحة تُخدَّم بنوع MIME القديم text/html -بغض النظر عن doctype أو بنية الوسوم- ستُفسَّر باستعمال مُفسِّر HTML المُتساهل، وسيتم تجاهل الأخطاء دون إظهار أيّة رسالة، ودون تحذير المستخدم (أو أي شخص آخر) حتى لو كانت الصفحة فيها أخطاء تقنية. فتحت XHTML 1.0 هذه النافذة، ولكن XHTML 1.1 أغلقتها، والنسخة التي لن تُصدَر XHTML 2.0 استمرت في منهج عدم التساهل في الأخطاء، وهذا هو السبب وراء ادعاء ملايين الصفحات على أنها XHTML 1.0 وأنَّ حفنة قليلة منهم هي XHTML 1.1 (أو XHTML 2.0)، فهل أنت تستعمل XHTML حقًا؟ تحقق من نوع MIME (في الواقع، إن لم تكن تعرف ما هو نوع MIME الذي تستعمله، فأنا أضمن لك أنك ما زلتَ تستعمل text/html)، فما لم تُخدِّم صفحاتك بنوع MIME الجديد application/xhtml+xml، فإن «XHTML» هي XML بالاسم فقط. رؤية تنافسية في حزيران/يونيو 2004، أقامت W3C ورشة عمل حول تطبيقات الويب والمستندات المجمّعة، حضر هذا الاجتماع ممثلون عن مصنعي ثلاثة متصفحات، وشركات تطوير الويب، وأعضاء آخرون من W3C، ومجموعة من الجهات المهتمة بما في ذلك منظمة Mozilla وشركة Opera Software، وأعطوا رؤيتهم لمستقبل الويب: تطوير معيار HTML 4 لتضمين ميزات جديدة تساعد مطوري تطبيقات الويب الحديثة. تمثِّل المبادئ السبعة الآتية ما نظن أنه أهم المتطلبات اللازمة لهذا العمل: التوافقية مع الإصدارات القديمة، ومسار واضح للهجرة يجب أن تكون تقنيات تطبيقات الويب مبنية على تقنيات مألوفة للمطورين، بما في ذلك HTML و CSS و DOM و JavaScript. يجب أن تُطبَّق الميزات الأساسية لتطبيقات الويب باستعمال السكربتات وصفحات الأنماط في IE6، لذا يكون هنالك مسارٌ واضحٌ للهجرة نصب عيني المطورين. أي حل لا يمكن أن يستعمل مع المتصفحات ذات الشعبية الكبيرة حاليًا دون حاجة إلى إضافات لن يكون ناجحًا. نظام معالجة أخطاء مُحدَّد بدقة يجب تفصيل كيفية معالجة الأخطاء إلى درجة لا تحتاج المتصفحات فيها إلى اختراع نظام معالجة أخطاء خاص بهم، أو أن يبنوا واحدًا مشتقًا من المتصفحات الأخرى. لا يجب عرض الأخطاء على المستخدمين يجب أن تُحدِّد المعايير السلوك اللازم اتباعه عند حدوث أي نوع من الأخطاء، ويجب أن يكون نظام معالجة الأخطاء متساهلًا (كما في CSS) بدلًا من أن يكون واضحًا للمستخدم وكارثيًا (كما في XML). الاستعمال العملي يجب أن تُبرَّر إضافة كل ميزة إلى معايير تطبيقات الويب بحالات الاستعمال العملي لها، لكن العكس ليس صحيحًا بالضرورة: ليست كل حالة استخدام تتطلب إنشاء ميزة جديدة. يُفضَّل أن تكون حالات الاستخدام ذات أساس في مواقع حقيقة استخدم فيها المطورون حلًا ليس مثاليًا للالتفاف على القصور في التقنية. سيبقى استخدام السكربتات قائمًا لكن يجب الابتعاد عنه إذا توفرت وسوم مناسبة. ويجب أن لا تتدخل السكربتات في طريقة العرض، وأن تكون مستقلة عن الجهاز المشغل لها. يجب تجنب التفريق بين الأجهزة يجب أن يتمكن المطورون من استخدام نفس الميزات الموجودة في نسخة سطح المكتب ونسخة الهواتف المحمولة لنفس المتصفح. عملية مفتوحة استفاد الويب من كونه مطوَّرًا في بيئة مفتوحة. وستصبح تطبيقات الويب أساسية في المستقبل، ويجب أن يكون تطويرها في بيئةٍ مفتوحةٍ أيضًا، ويجب إتاحة القوائم البريدية والأرشيفات ومسودات المعايير للجميع. سُئِلَ المشاركون في ورشة العمل في استطلاعٍ للرأي: «هل يجب على W3C تطوير إضافات لوظائف جاهزة في HTML و CSS، وإضافات لأساس DOM لكي تتحقق متطلبات تطوير تطبيقات الويب متوسطة التعقيد. أم عليها تطوير واجهة برمجية (API) كاملة على مستوى النظام (OS-level)؟ (هذا الاقتراح من Ian Hickson، من Opera Software)» كان التصويت 11 إلى 8 ضد هذا الاقتراح، وكتبت W3C في ملخص الورشة: وبعد هذا القرار، كان لدى الأشخاص الذين اقترحوا تطوير HTML ونماذج HTML خياران فقط: الاستسلام، أو إكمال عملهم خارج إطار W3C، وقرروا المضي قدمًا في الخيار الثاني، وسجلوا النطاق whatwg.org، وفي حزيران/يونيو 2004، وُلِدَت مجموعة عمل WHAT. مجموعة عمل WHAT ما هي مجموعة عمل WHAT؟ سأقتبس من كلامهم: الجملة الأساسية في الفقرة السابقة هي "دون التسبب بمشاكل تتعلق بالتوافقية"، ليست XHTML (دون الثغرة التي وفرها الملحق C) متوافقةً مع HTML، وتتطلب نوع MIME خاص بها، وXForms ليست متوافقة مع نماذج HTML لعدم القدرة على استعمالها إلا مع الصفحات المُخدَّمة بنوع MIME الخاص بصفحات XHTML، هذا يعني أن XForms تتطلب عدم التساهل في التعامل مع الأخطاء. فبدلًا من أن نرمي ما يقارب عقدًا من الزمن من العمل على HTML ونجعل 99% من صفحات الويب الموجودة حاليًا غير قابلة للاستخدام، قررت مجموعة عمل WHAT أن تنتهج منهجًا آخر: توثيق خوارزميات التعامل مع الأخطاء «المتسامحة» التي تستعملها المتصفحات. كانت وما زالت المتصفحات متساهلةً مع أخطاء HTML، لكن لم يُتعِب أحدٌ نفسه ويوثق آلية التساهل بالضبط. لدى متصفح NCSA Mosaic خوارزميات خاصة به للتعامل مع الأخطاء، وحاول Netscape أن يقلده، ثم حاول Internet Explorer تقليد Netscape، ثم حاول Opera و Firefox تقليد Internet Explorer، ثم حاول Safari تقليد Firefox، وهلّم جرًا حتى يومنا هذا. إذ ضيع المطورون آلاف الساعات محاولين جعل منتجاتهم متوافقة مع منتجات منافسيهم. استغرقت مجموعة عمل WHAT خمسة أعوامٍ من العمل للنجاح في توثيق كيفية تفسير HTML (ما عدا بعض الحالات الخاصة جدًا والغامضة) بطريقة متوافقة مع جميع المحتوى الموجود على الويب. فلا يوجد في الخوارزمية النهائية أيّة حالة يتوقف فيها مفسر HTML عن إكمال تفسير صفحة HTML ويعرض رسالة خطأ للمستخدم النهائي. وفي الفترة التي كانت تجرى فيها الهندسة العكسية، كانت تعمل مجموعة WHAT بصمت على أشياءٍ أخرى أيضًا، منها معيارٌ سُمي في بادئ الأمر "Web Forms 2.0" الذي أضاف بعض عناصر التحكم إلى نماذج HTML (ستتعلم المزيد عن نماذج HTML في درسٍ لاحق). ومسودة معيار أخرى باسم "Web Applications 1.0" التي حَوَت ميزات أخرى رئيسية مثل canvas والدعم المُضمَّن لتشغيل الصوت والفيديو دون إضافات. العودة إلى W3C لمدة تقارب السنتين ونصف، تجاهلت W3C ومجموعة عمل WHAT بعضهما البعض، على الرغم من أن مجموعة عمل WHAT كانت تركز على نماذج الويب وميزات HTML الجديدة، ومجموعة عمل W3C مشغولة بإصدار 2.0 من XHTML، لكن بحلول تشرين الأول/أكتوبر، كان جليًا أن مجموعة عمل WHAT قد أحدثت زخمًا كبيرًا، بينما كانت XHTML 2.0 تقبع على شكل مسودة لم يتم تطبيقها من أيّ متصفح رئيسي. في تشرين الأول/أكتوبر، أعلن Tim Berners-Lee -مؤسس W3C- أن W3C ستعمل مع مجموعة عمل WHAT لتطوير HTML. واحد من أول الأشياء التي قررت مجموعة عمل W3C الجديدة فعلها هي إعادة تسمية "Web Applications 1.0" إلى "HTML5"، ومن هنا بدأت رحلة هذا السلسلة. حاشية في تشرين الأول/أكتوبر 2009، أغلقت W3C مجموعة عمل XHTML 2 وأصدرت هذه الإفادة لشرح قرارها: مراجع إضافية The History of the Web، مسودة قديمة كتبها Ian Hickson HTML/History كتبها Michael Smith، و Henri Sivonen، وآخرون A Brief History of HTML لكاتبها Scott Reynen ترجمة -وبتصرّف- لفصل A Quite Biased History of HTML5 من كتاب Dive Into HTML5 لمؤلفه Mark Pilgrim.
  10. ‏AJAX‏AJAX هي اختصار للعبارة "asynchronous JavaScript and XML"، وهي وسيلة لجلب البيانات من الخادوم دون الحاجة لإعادة تحميل الصّفحة، وهي تقوم على استخدام كائن مُتاح في المتصفّح اسمه XMLHttpRequest (أو XHR اختصارًا) لإرسال الطّلب إلى الخادوم ثمّ التّعامل مع البيانات الّتي يُجيب بها الخادوم. تُوفّر jQuery الوظيفة ‎$.ajax‎ (ووظائف أخرى مرافقة مُختصرة) لتسهيل العمل مع طلبات XHR في جميع المتصفّحات. ‏‎$.ajax‎بإمكاننا استخدام الوظيفة ‎$.ajax()‎ المُرفقة مع jQuery بعدّة أساليب: إحداها أن نُمرّر إليها كائنًا يحوي الإعدادات فقط، أو أن نُمرّر الرّابط مع أو بدون كائن الإعدادات. لنُلقِ نظرة على الأسلوب الأول: // أنشئ دالّة الاستدعاء الرّاجع الّتي ستُنفّذ عندما ينجح طلب AJAX var updatePage = function( resp ) { $( '#target').html( resp.people[0].name ); }; // وعندما يفشل var printError = function( req, status, err ) { console.log( 'something went wrong', status, err ); }; // أنشئ كائن الإعدادات الذي يصف الطّلب var ajaxOptions = { url: '/data/people.json', dataType: 'json', success: updatePage, error: printError }; // أرسل الطّلب $.ajax(ajaxOptions);بإمكانك طبعًا أن تُمرّر كائنًا حرفيًّا مباشرةً إلى الوظيفة‎$.ajax() ‎ وأن تستخدم دالّة مجهولة محلّ success وerror، هذا الأسلوب كتابته أسهل، وصيانته في المستقبل أسهل: $.ajax({ url: '/data/people.json', dataType: 'json', success: function( resp ) { $( '#target').html( resp.people[0].name ); }, error: function( req, status, err ) { console.log( 'something went wrong', status, err ); } });كما قلنا، بإمكانك استخدام الوظيفة‎$.ajax() ‎ بأسلوب ثانٍ، وذلك بتمرير الرّابط أوّلًا ثمّ كائن الإعدادات ثانيًا (ليس إلزاميًّا). يُفيدك هذا في حال رغبت في استخدام الإعدادات المبدئيّة للوظيفة أو في حال رغبت في استخدام كائن الإعدادات نفسه لأكثر من رابط: $.ajax( '/data/people.json', { type: 'GET', dataType: 'json', success: function( resp ) { console.log( resp.people ); }, error: function( req, status, err ) { console.log( 'something went wrong', status, err ); } });في المثال السّابق، لا تشترط الوظيفة سوى الرّابط، ولكنّ إضافة كائن الإعدادات تسمح لنا بإخبار jQuery بنوع البيانات الّتي نُرسلها، وأي فعل HTTP نستخدمه (POST، GET، إلخ...‏‏)، وما نوع البيانات الّتي نتوقّع استقبالها من الخادوم، وما الّذي يجب فعله إن نجح الطّلب أو فشل... اطّلع على وثائق الوظيفة‎$.ajax() ‎ لقراءة كامل الخيارات الّتي يمكن إضافتها إلى كائن الإعدادات. ‏A في AJAX تعني "لامتزامن"تجري طلبات AJAX بصورة لا متزامنة، وهذا يعني أنّ الوظيفة‎$.ajax ‎ تنتهي قبل انتهاء الطّلب، وقبل أن تُستدعى دّالة success، أيّ أنّ جملة return تُنفّذ قبل أن يصل جواب الطّلب. فالدّالة getSomeData في المثال التّالي ستُعيد قيمة data قبل أن تُعرّف، مما يؤدّي إلى وقوع خطأ: تحذير: نصّ برمجيّ غير سليم var getSomeData = function() { var data; $.ajax({ url: '/data/people.json', dataType: 'json', success: function(resp) { data = resp.people; } }); return data; } $( '#target' ).html( getSomeData().people[0].name );‏X في AJAX تعني JSON!وضع المصطلح AJAX عام 2005 ليصف طريقة لجلب البيانات من الخادوم دون الحاجة لإعادة تحميل كامل الصّفحة. في ذلك الوقت، كانت الصّيغة الأكثر شيوعًا للبيانات الّتي تُرسلها الخوادم هي XML، أمّا اليوم، فإنّ JSON هي الصّيغة الّتي تعتمدها أكثر التّطبيقات الحديثة. صيغة JSON في أساسها هي سلسلة نصّيّة (string) تُمثّل البيانات، وتبدو مُشابهة كثيرًا لكائن JavaScript عاديّ، ولكنّها لا تستطيع تمثيل كلّ أنواع البيانات الّتي يستطيع كائن JavaScript تمثيلها. فمثلًا: لا يمكن لـJSON تمثيل كائنات التّاريخ (Date) ولا الدّوال (functions). فيما يلي مثال عن نصّ JSON، لاحظ كيف تُحاط كلّ أسماء الخصائص بعلامتي اقتباس مُضاعفتين: { "people" : [ { "name" : "Ben", "url" : "http://benalman.com/", "bio" : "I create groovy websites, useful jQuery plugins, and play a mean funk bass. I'm also Director of Pluginization at @bocoup." }, { "name" : "Rebecca", "url" : "http://rmurphey.com", "bio" : "Senior JS dev at Bocoup" }, { "name" : "Jory", "url" : "http://joryburson.com", "bio" : "super-enthusiastic about open web education @bocoup. lover of media, art, and fake mustaches." } ] }تذكّر أنّ JSON هو تمثيل نصّيّ لكائن، ما يعني أنّه يجب تفسير السّلسلة النّصيّة لتحويلها إلى كائن JavaScript عاديّ قبل التّعامل معها. عندما تعمل مع جواب ورد من الخادوم بصيغة JSON، فإنّ jQuery تتولّى هذه المهمّة عنك. ولكن من المهمّ التمييز بين الكائنات الفعليّة، وطريقة تمثيلها في JSON. ملاحظة: إن أردت إنشاء سلسلة JSON نصّيّة من كائن JavaScript أو تفسير سلسلة JSON نصّيّة لتحويلها إلى كائن JavaScript دون الاستعانة بـjQuery، فإنّ المُتصفّحات الحديثة تُقدّم الوظيفتين ‎JSON.stringify()‎ و‎JSON.parse()‎، ويمكن إضافة هذه الخصائص إلى المُتصفّحات القديمة باستخدام المكتبة json2.js. توفّر jQuery أيضًا وظيفة ‎jQuery.parseJSON()‎، الّتي توافق الوظيفة ‎JSON.parse()‎ في المتصفّحات، إلّا أنّها لا توفّر وظيفة تُقابل ‎JSON.stringify()‎. وظائف مُختصرةإن كان كلّ ما نريده إرسال طلب بسيط، دون الاهتمام بالتّعامل مع الأخطاء الّتي قد تقع، فإنّ jQuery تُوفّر وظائف مُختصرة تسمح لنا بفعل ذلك. تستقبل كل وظيفة مُختصرة رابطًا وكائن إعدادات غير إلزاميّ، ودالّة تُستدعى عند نجاح الطّلب فقط: $.get( '/data/people.html', function( html ){ $( '#target' ).html( html ); }); $.post( '/data/save', { name: 'Rebecca' }, function( resp ) { console.log( resp ); });إرسال البيانات والعمل مع النّماذجبإمكاننا إرسال بيانات مع طلبنا بتعيين قيمة للخاصة data في كائن الإعدادات، أو تمرير كائن كمُعامل ثانٍ للوظائف المُختصرة. ستُضاف هذه البيانات إلى الرّابط في طلبات GET بصورة "جملة استعلام" (query string)، أمّا في طلبات POST فإنّها ستُرسل كبيانات نموذج. توفّر jQuery وظيفة مُفيدة ‎.serialize()‎ الّتي تستقبل مُدخلات نموذج وتُحوّلها إلى صيغة "جملة استعلام" (مثل field1name=field1value&field2name=field2value...): $( 'form' ).submit(function( event ) { event.preventDefault(); var form = $( this ); $.ajax({ type: 'POST', url: '/data/save', data: form.serialize(), dataType: 'json', success: function( resp ) { console.log( resp ); } }); });‏jqXHRتُعيد‎$.ajax() ‎ والوظائف المُختصرة المرافقة لها، كائن jqXHR (اختصارًا لـjQuery XML HTTP Request) والّذي يتضمّن وظائف مُفيدةً كثيرة. بإمكاننا إرسال طلب باستخدام ‎$.ajax()‎ ثمّ حفظ كائن jqXHR في مُتغيّر: var req = $.ajax({ url: '/data/people.json', dataType: 'json' });بإمكاننا استخدام هذا العنصر لربط الاستدعاءات الرّاجعة بالطّلب، حتّى بعد أن يكتمل الطّلب. بإمكاننا مثلًا استخدام الوظيفة ‎.then()‎ (ثُمَّ) لإرفاق استدعاءي نجاح الطّلب وفشله، إذ تقبل ‎.then()‎ دالّة أو اثنتين، تستدعى الأولى عند نجاح الطّلب، والثّانية إن فشل: var success = function( resp ) { $( '#target' ).append( '<p>people: ' + resp.people.length + '</p>' ); console.log( resp.people ); }; var err = function( req, status, err ) { $( '#target' ).append( '<p>something went wrong</p>' ); }; req.then( success, err ); req.then(function() { $( '#target' ).append( '<p>it worked</p>' ); });بإمكاننا استدعاء ‎.then()‎ على كائن الطّلب قدر ما نشاء، وستُنفّذ الاستدعاءات الرّاجعة بالتّرتيب ذاته الّتي أرفقت وفقه. إن لم نُرد إرفاق استدعاءي النّجاح والفشل معًا، فبإمكاننا استخدام الوظيفتين ‎.done()‎ و‎.fail()‎ على كائن الطّلب: req.done( success ); req.fail( err );لو أردنا إرفاق استدعاء راجعٍ يُنفَّذ دومًا، بغض النّظر عن نجاح الطّلب أو فشله، فيمكننا استخدام الوظيفة ‎.always()‎ على كائن الطّلب: req.always(function() { $( '#target' ) .append( '<p>one way or another, it is done now</p>' ); });‏JSONPيستغرب كثيرٌ من المبتدئين في JavaScript فشل طلبات XHR الّتي يرسلونها إلى نطاق آخر على الإنترنت، فمثلاً: يحاول بعض المُطوّرين جلب بيانات من واجهة برمجيّة من طرف ثالث (third-party API)، ليفاجؤوا بفشل الطّلب باستمرار. السّبب وراء ذلك أنّ المُتصفّحات لا تسمح بإرسال طلبات XHR إلى نطاقات إنترنت أخرى لأسباب أمنيّة، ولكنّ بعض الواجهات البرمجيّة تُعيد البيانات بصيغة JSONP (اختصارًا لـJSON with Padding)، الّتي تسمح للمُطوّرين بجلب البيانات متجاوزين حظر المُتصفّح. الحقيقة أن JSONP ليس طلب AJAX فعليًّا، فهو لا يستخدم طلب XHR الّذي يوفّره المُتصفّح، بل يعمل بإدراج وسم <script> في صفحة الويب، الّذي يحوي بدوره البيانات المطلوبة، مُحاطة بدالّة تُعيد هذه البيانات عند استدعائها. ليس هذه التّفاصيل مهمّة الآن، لأنّ jQuery تسمح لك بطلب JSONP كما لو كان XHR باستخدام الوظيفة ‎$.ajax()‎ بتعيين نوع البيانات dataType إلى 'jsonp' في كائن الإعدادات. $.ajax({ url: '/data/search.jsonp', data: { q: 'a' }, dataType: 'jsonp', success: function( resp ) { $( '#target' ).html( 'Results: ' + resp.results.length ); } });ملاحظة: عادةً ما توفّر الواجهات البرمجيّة خيارًا لتعيين اسم الدّالّة الّتي تُحيط بالبيانات والّتي ستُستدعى في عنوان الرّابط. عادةً ما يكون هذا اسم مُعامل الرّابط callback، وهذا ما تتوقّعه jQuery مبدئيًّا، إلّا أن بإمكانك تغييره بتعيين قيمة للخاصة jsonp في كائن الإعدادات الّذي تُمرّره لـ ‎$.ajax()‎. بإمكانك أيضًا استخدام الوظيفة المُختصرة ‎$.getJSON()‎ لإرسال طلب JSONP، حيث تستطيع jQuery تمييزه من خلال وجود ‎callback=?‎ أو ما يشبهها في الرّابط: $.getJSON( '/data/search.jsonp?q=a&callback=?', function( resp ) { $( '#target' ).html( 'Results: ' + resp.results.length ); } );مشاركة الموارد عبر الأصول (cross-origin resource sharing أو CORS اختصارًا) هي خيارٌ آخر للسّماح بالطّلبات العابرة للأصول. ولكنّها غير مدعومة في المتصفّحات القديمة، كما أنّها تحتاج تهيئة خاصّة على الخادوم وتعديل ترويسات الطّلبات في XHR لتعمل. الكائنات المُؤجّلة (Deferreds)ليست كائنات jqXHR الّتي تعرّفنا عليها إلا "نكهة" خاصّة ممّا يُعرف "بالكائنات المؤجّلة". تسمح jQuery لك بإنشاء كائنات مؤجّلة بنفسك، والّتي يمكن الاستفادة منها في تسهيل التّعامل مع الأوامر اللامتزامنة، فهي توفّر طريقة للاستجابة لعمليّة تجري بصورة غير متزامنة بعد نجاحها أو فشلها، وتجنّبك الحاجة لكتابة استدعاءات راجعة مُتداخلة فيما بينها. ‏‎$.Deferred‎بإمكانك إنشاء كائنٍ مؤجّل باستخدام ‏‎$.Deferred()‎. في المثال التّالي نُنفّذ دالة داخل setTimeout، ثمّ "نفي" (resolve) بوعدنا بإعادة القيمة الّتي تُرجعها الدّالة هذه. نُعيد الوعد (promise)، وهو كائن يمكن ربط الاستدعاءات الرّاجعة به، ولكنّه لا يؤثّر في نتيجة الكائن المؤجّل بحدّ ذاته. بإمكاننا "الإخلاف" (reject) بالوعد إذا وقع خطأ ما أثناء عمل الدّالّة: function doSomethingLater( fn, time ) { var dfd = $.Deferred(); setTimeout(function() { dfd.resolve( fn() ); }, time || 0); return dfd.promise(); } var promise = doSomethingLater(function() { console.log( 'This function will be called in 100ms' ); }, 100);‏‎.then()‎ و‎.done()‎ و‎.fail()‎ و‎.always()‎يمكننا ربط دوالّ تتولّى حالات الخطأ والنّجاح بالوعود، تمامًا كما في كائنات jqXHR: function doSomethingLater( fn, time ) { var dfd = $.Deferred(); setTimeout(function() { dfd.resolve( fn() ); }, time || 0); return dfd.promise(); } var success = function( resp ) { $( '#target' ).html( 'it worked' ); }; var err = function( req, status, err ) { $( '#target' ).html( 'it failed' ); }; var dfd = doSomethingLater(function() { /* ... */ }, 100); dfd.then( success, err );‏‎.pipe()‎بإمكاننا استخدام الوظيفة ‏‎.pipe()‎ للوعود للاستجابة إلى القيمة الّتي تُوفى وذلك بتعديلها ثمّ إعادة كائن مؤجّل جديد. تعمل الوظيفة ‏‎.then()‎ بدءًا من الإصدارة 1.8 من jQuery كما تعمل الوظيفة ‏‎.pipe()‎. function doSomethingLater( fn, time ) { var dfd = $.Deferred(); setTimeout(function() { dfd.resolve( fn() ); }, time || 0); return dfd.promise(); } var dfd = doSomethingLater(function() { return 1; }, 100); dfd .pipe(function(resp) { return resp + ' ' + resp; }) .done(function(upperCaseResp) { $( '#target' ).html( upperCaseResp ); });التّعامل مع العمليّات الّتي قد تكون لامتزامنةأحيانًا تكون لدينا وظيفة قد تعمل بصورة متزامنة أو لا متزامنة وفق ظروف مُعيّنة، فمثلًا: دالّة تقوم بعمليّة لا متزامنة أوّل مرّة تُستدعى فيها، ثمّ تُخزّن القيمة الّتي أنتجتها العمليّة لتُعيدها مباشرةً عند استدعاءها مُستقبلًا. في هذه الحالة يمكننا الاستفادة من ‎$.when()‎ للاستجابة لكلا الحالتين: function maybeAsync( num ) { var dfd = $.Deferred(); // أعِد وعدًا مؤجّلًا عندما num === 1 if ( num === 1 ) { setTimeout(function() { dfd.resolve( num ); }, 100); return dfd.promise(); } // أنهِ مباشرة فيما سوى ذلك، مُعيدًا num return num; } // هذا سيُجرى بصورة غير متزامنة ويعِد بإعادة 1 $.when( maybeAsync( 1 ) ).then(function( resp ) { $( '#target' ).append( '<p>' + resp + '</p>' ); }); // هذا سُيعيد 0 مُباشرةً $.when( maybeAsync( 0 ) ).then(function( resp ) { $( '#target' ).append( '<p>' + resp + '</p>' ); });بإمكانك أيضًا تمرير أكثر من معامل إلى ‎$.when()‎، الأمر الّذي يسمح لك بدمج عمليّات متزامنة ولا متزامنة معًا ثمّ الحصول على نتائج تنفيذها كلّها كُمعاملات للاستدعاء الرّاجع: function maybeAsync( num ) { var dfd = $.Deferred(); // أعد وعدًا مؤجّلًا عندما num === 1 if ( num === 1 ) { setTimeout(function() { dfd.resolve( num ); }, 100); return dfd.promise(); } // أنهِ مباشرةً فيما سوى ذلك، مُعيدًا num return num; } $.when( maybeAsync( 0 ), maybeAsync( 1 ) ) .then(function( resp1, resp2 ) { var target = $( '#target' ); target.append( '<p>' + resp1 + '</p>' ); target.append( '<p>' + resp2 + '</p>' ); });عندما يكون إحدى مُعاملات ‎$.when()‎ كائن jqXHR، فإنّنا نحصل على مصفوفة من المُعاملات تُمرّر إلى استدعائنا الرّاجع: function maybeAsync( num ) { var dfd = $.Deferred(); // أعد وعدًا مؤجّلًا عندما num === 1 if ( num === 1 ) { setTimeout(function() { dfd.resolve( num ); }, 100); return dfd.promise(); } // أنهِ مباشرةً فيما سوى ذلك، مُعيدًا num return num; } $.when( maybeAsync( 0 ), $.get( '/data/people.json' ) ) .then(function( resp1, resp2 ) { console.log( "Both operations are done", resp1, resp2 ); });مصادر إضافيةتوثيق AJAXكائن jqXHRالكائنات المؤجّلة في jQueryترجمة (بشيء من التصرف) للجزء السادس من سلسلة  jQuery Fundamentals لمؤلّفتها Rebecca Murphey.