تخصيص Android: كيفية إنشاء مكون واجهة مستخدم يفعل ما تريد

نشرت: 2022-03-11

ليس من غير المألوف للمطورين أن يجدوا أنفسهم في حاجة إلى مكون واجهة مستخدم لا يتم توفيره بواسطة النظام الأساسي الذي يستهدفونه أو يتم توفيره بالفعل ، ولكنه يفتقر إلى خاصية أو سلوك معين. الإجابة على كلا السيناريوهين هي مكون مخصص لواجهة المستخدم.

نموذج واجهة مستخدم Android قابل للتخصيص بطبيعته ، حيث يوفر وسائل تخصيص واختبار Android والقدرة على إنشاء مكونات واجهة مستخدم مخصصة بطرق مختلفة:

  • ترث مكونًا موجودًا ( TextView و ImageView وما إلى ذلك) ، وقم بإضافة / تجاوز الوظائف المطلوبة. على سبيل المثال ، CircleImageView الذي يرث ImageView ، يلغي onDraw() لتقييد الصورة المعروضة بدائرة ، وإضافة وظيفة loadFromFile() لتحميل صورة من الذاكرة الخارجية.

  • إنشاء مكون مركب من عدة مكونات. عادةً ما يستفيد هذا الأسلوب من التخطيطات للتحكم في كيفية ترتيب المكونات على الشاشة. على سبيل المثال ، LabeledEditText الذي يرث LinearLayout مع اتجاه أفقي ، ويحتوي على كل من TextView يعمل كتسمية و EditText يعمل كحقل إدخال نص.

    يمكن أن يستفيد هذا النهج أيضًا من الطريقة السابقة ، على سبيل المثال ، يمكن أن تكون المكونات الداخلية أصلية أو مخصصة.

  • النهج الأكثر تنوعًا والأكثر تعقيدًا هو إنشاء مكون مرسوم ذاتيًا . في هذه الحالة ، سيرث المكون فئة View العامة ووظائف تجاوز مثل onMeasure() لتحديد تخطيطه ، و onDraw() لعرض محتوياته ، وما إلى ذلك. عادةً ما تعتمد المكونات التي تم إنشاؤها بهذه الطريقة بشكل كبير على واجهة برمجة تطبيقات الرسم ثنائية الأبعاد في Android.

دراسة حالة تخصيص Android: CalendarView

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

على سبيل المثال ، لا يوفر مكون CalendarView أي طريقة لتغيير كيفية تمييز يوم معين ، أو لون الخلفية الذي يجب استخدامه. لا توجد أيضًا طريقة لإضافة أي نص أو رسومات مخصصة ، لتمييز مناسبة خاصة ، على سبيل المثال. باختصار ، يبدو المكون هكذا ، ولا يمكن تغيير أي شيء تقريبًا:

لقطة شاشة
CalendarView في AppCompact.Light theme.

اصنع خاصتك

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

للمتابعة ، يمكنك العثور على شفرة المصدر هنا.

1. تخطيط المكون

أولاً ، لنبدأ كيف يبدو المكون. لتبسيط الأمر ، دعنا نعرض الأيام في شبكة ، وفي الجزء العلوي ، اسم الشهر جنبًا إلى جنب مع أزرار "الشهر التالي" و "الشهر السابق".

لقطة شاشة
عرض التقويم المخصص.

يتم تحديد هذا التخطيط في ملف control_calendar.xml ، على النحو التالي. لاحظ أنه تم اختصار بعض العلامات المتكررة بـ ... :

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. فئة المكون

يمكن تضمين التخطيط السابق كما هو في Activity أو Fragment وسيعمل بشكل جيد. لكن تغليفها كمكون مستقل لواجهة المستخدم سيمنع تكرار الكود ويسمح بتصميم معياري ، حيث تتولى كل وحدة مسؤولية واحدة.

سيكون مكون واجهة المستخدم الخاص بنا عبارة عن LinearLayout ، لمطابقة جذر ملف تخطيط XML. لاحظ أن الأجزاء المهمة فقط هي التي تظهر من الكود. تنفيذ المكون يكمن في CalendarView.java :

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

الشفرة واضحة ومباشرة. عند الإنشاء ، يضخم المكون تخطيط XML ، وعندما يتم ذلك ، فإنه يقوم بتعيين الضوابط الداخلية للمتغيرات المحلية لتسهيل الوصول إليها لاحقًا.

3. بعض المنطق مطلوب

لجعل هذا المكون يعمل بالفعل كطريقة عرض تقويم ، يتم ترتيب بعض منطق الأعمال. قد يبدو الأمر معقدًا في البداية ، لكن ليس هناك الكثير في الحقيقة. دعنا نقسمها:

  1. عرض التقويم بعرض سبعة أيام ، ومن المؤكد أن جميع الأشهر ستبدأ في مكان ما في الصف الأول.

  2. أولاً ، نحتاج إلى معرفة الموضع الذي يبدأ فيه الشهر ، ثم ملء جميع المراكز قبل ذلك بالأرقام من الشهر السابق (30 ، 29 ، 28 .. إلخ) حتى نصل إلى المركز 0.

  3. ثم نملأ أيام الشهر الحالي (1 ، 2 ، 3 ... إلخ).

  4. بعد ذلك تأتي أيام الشهر التالي (مرة أخرى ، 1 ، 2 ، 3 .. إلخ) ، ولكن هذه المرة نقوم فقط بملء المواضع المتبقية في الصف (الصفوف) الأخير من الشبكة.

يوضح الرسم البياني التالي تلك الخطوات:

لقطة شاشة
عرض التقويم المخصص منطق الأعمال.

تم تحديد عرض الشبكة بالفعل ليكون سبع خلايا ، مما يدل على التقويم الأسبوعي ، ولكن ماذا عن الارتفاع؟ يمكن تحديد الحجم الأكبر للشبكة بأسوأ سيناريو لشهر مدته 31 يومًا بدءًا من يوم السبت ، وهي الخلية الأخيرة في الصف الأول ، وستحتاج إلى 5 صفوف أخرى لعرضها بالكامل. لذا ، فإن إعداد التقويم لعرض ستة صفوف (بإجمالي 42 يومًا) سيكون كافيًا للتعامل مع جميع الحالات.

لكن ليس كل الشهور بها 31 يومًا! يمكننا تجنب المضاعفات الناشئة عن ذلك باستخدام وظيفة التاريخ المضمنة في Android ، وتجنب الحاجة إلى معرفة عدد الأيام بأنفسنا.

كما ذكرنا سابقًا ، فإن وظائف التاريخ التي يوفرها فصل Calendar تجعل التنفيذ بسيطًا جدًا. في المكون الخاص بنا ، تنفذ وظيفة updateCalendar() هذا المنطق:

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. للتخصيص في القلب

نظرًا لأن المكون المسؤول عن عرض الأيام الفردية هو GridView ، فإن Adapter يعد مكانًا جيدًا لتخصيص كيفية عرض الأيام ، نظرًا لأنه مسؤول عن الاحتفاظ بالبيانات وتضخيم طرق العرض لخلايا الشبكة الفردية.

في هذا المثال ، سنطلب ما يلي من CalendearView بنا:

  • يجب أن يكون اليوم الحالي بخط غامق باللون الأزرق .
  • يجب أن تكون الأيام خارج الشهر الحالي باللون الرمادي .
  • يجب أن تعرض الأيام المصحوبة بحدث رمزًا خاصًا.
  • يجب تغيير ألوان رأس التقويم حسب الموسم (الصيف ، الخريف ، الشتاء ، الربيع).

من السهل تحقيق المتطلبات الثلاثة الأولى من خلال تغيير سمات النص وموارد الخلفية. دعونا ننفذ CalendarAdapter لتنفيذ هذه المهمة. إنه أمر بسيط لدرجة أنه يمكن أن يكون فئة عضو في CalendarView . من خلال تجاوز وظيفة getView() ، يمكننا تحقيق المتطلبات المذكورة أعلاه:

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

يتطلب التصميم النهائي مزيدًا من العمل. أولاً ، دعنا نضيف ألوان الفصول الأربعة في /res/values/colors.xml :

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

بعد ذلك ، دعنا نستخدم مصفوفة لتحديد الموسم لكل شهر (بافتراض نصف الكرة الشمالي ، من أجل التبسيط ؛ آسف يا أستراليا!). في CalendarView نضيف متغيرات الأعضاء التالية:

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

بهذه الطريقة ، يتم تحديد اللون المناسب عن طريق تحديد الموسم المناسب ( monthSeason[currentMonth] ) ثم اختيار اللون المقابل ( rainbow[monthSeason[currentMonth] ) ، تتم إضافة هذا إلى updateCalendar() للتأكد من تحديد اللون المناسب كلما تم تغيير التقويم.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

وبذلك نحصل على النتيجة التالية:

تخصيص Android
يتغير لون الرأس حسب الموسم.

ملاحظة مهمة نظرًا للطريقة التي يقارن بها HashSet الكائنات ، فإن التحقق أعلاه من eventDays.contains(date) في updateCalendar() لن ينتج عن كائنات التاريخ ما لم تكن متطابقة تمامًا. لا يقوم بإجراء أي فحوصات خاصة لنوع بيانات Date . للتغلب على هذا ، يتم استبدال هذا الاختيار بالرمز التالي:

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. تبدو قبيحة في وقت التصميم

قد يكون اختيار Android للعناصر النائبة في وقت التصميم موضع تساؤل. لحسن الحظ ، يقوم Android بالفعل بإنشاء مثيل للمكون الخاص بنا من أجل عرضه في مصمم واجهة المستخدم ، ويمكننا استغلال ذلك عن طريق استدعاء updateCalendar() في مُنشئ المكون. بهذه الطريقة سيكون المكون منطقيًا بالفعل في وقت التصميم.

لقطة شاشة

إذا كانت تهيئة المكون تستدعي الكثير من المعالجة أو تحمل الكثير من البيانات ، فيمكن أن تؤثر على أداء IDE. في هذه الحالة ، يوفر Android وظيفة أنيقة تسمى isInEditMode() والتي يمكن استخدامها للحد من كمية البيانات المستخدمة عندما يتم إنشاء مثيل للمكون بالفعل في مصمم واجهة المستخدم. على سبيل المثال ، إذا كان هناك الكثير من الأحداث ليتم تحميلها في CalendarView ، فيمكننا استخدام isInEditMode() داخل وظيفة updateCalendar() لتوفير قائمة أحداث فارغة / محدودة في وضع التصميم ، وتحميل الحدث الحقيقي بخلاف ذلك.

6. استدعاء المكون

يمكن تضمين المكون في ملفات تخطيط XML (يمكن العثور على نموذج لاستخدام في activity_main.xml ):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

ويتم استرجاعه ليتم التفاعل معه بمجرد تحميل التخطيط:

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

يقوم الكود أعلاه بإنشاء HashSet للأحداث ، ويضيف اليوم الحالي إليه ، ثم يمرره إلى CalendarView . نتيجة لذلك ، سيعرض CalendarView اليوم الحالي باللون الأزرق الغامق ويضع علامة الحدث عليه:

لقطة شاشة
CalendarView عرض حدث

7. إضافة السمات

تتمثل إحدى الوسائل الأخرى التي يوفرها Android في تعيين سمات لمكون مخصص. يتيح ذلك لمطوري Android الذين يستخدمون المكون تحديد الإعدادات عبر تنسيق XML ورؤية النتيجة على الفور في مصمم واجهة المستخدم ، بدلاً من الاضطرار إلى الانتظار ورؤية كيف يبدو CalendarView في وقت التشغيل. دعنا نضيف القدرة على تغيير تنسيق التاريخ المعروض في المكون ، على سبيل المثال لتهجئة الاسم الكامل للشهر بدلاً من الاختصار المكون من ثلاثة أحرف.

للقيام بذلك ، يجب اتباع الخطوات التالية:

  • نعلن السمة. دعنا نسميها dateFormat ونعطيه نوع بيانات string . أضفه إلى /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • استخدم السمة في التخطيط الذي يستخدم المكون ، وامنحه القيمة "MMMM yyyy" :
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • أخيرًا ، اجعل المكون يستخدم قيمة السمة:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] أنشئ المشروع وستلاحظ تغييرات التاريخ المعروضة في مصمم واجهة المستخدم لاستخدام الاسم الكامل للشهر ، مثل "يوليو 2015". حاول تقديم قيم مختلفة وانظر ماذا يحدث.

لقطة شاشة
تغيير سمات CalendarView .

8. التفاعل مع المكون

هل حاولت الضغط في يوم معين؟ لا تزال عناصر واجهة المستخدم الداخلية في المكون الخاص بنا تتصرف بطريقتها العادية المتوقعة وستطلق الأحداث استجابةً لإجراءات المستخدم. إذن ، كيف نتعامل مع هذه الأحداث؟

تتكون الإجابة من جزأين:

  • التقاط الأحداث داخل المكون ، و
  • قم بالإبلاغ عن Activity إلى أصل المكون (يمكن أن يكون جزءًا أو Fragment أو حتى مكونًا آخر).

الجزء الأول بسيط جدًا. على سبيل المثال ، للتعامل مع عناصر الشبكة ذات الضغط الطويل ، نقوم بتعيين مستمع مطابق في فئة المكون لدينا:

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

هناك عدة طرق للإبلاغ عن الأحداث. واحد مباشر وبسيط هو نسخ الطريقة التي يعمل بها Android: فهو يوفر واجهة لأحداث المكون التي يتم تنفيذها من قبل والد المكون ( eventHandler في مقتطف الشفرة أعلاه).

يمكن تمرير وظائف الواجهة أي بيانات ذات صلة بالتطبيق. في حالتنا ، تحتاج الواجهة إلى كشف معالج حدث واحد ، والذي يتم تجاوزه بتاريخ اليوم المضغوط. يتم تحديد الواجهة التالية في CalendarView :

 public interface EventHandler { void onDayLongPress(Date date); }

يمكن توفير التنفيذ الذي يوفره الأصل لعرض التقويم عبر setEventHandler() . فيما يلي نموذج للاستخدام من "MainActivity.java":

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

سيؤدي الضغط لفترة طويلة على اليوم إلى إطلاق حدث الضغط الطويل الذي يتم التقاطه ومعالجته بواسطة GridView والإبلاغ عنه من خلال استدعاء onDayLongPress() في التنفيذ المقدم ، والذي بدوره سيعرض تاريخ يوم الضغط على الشاشة:

لقطة شاشة

هناك طريقة أخرى أكثر تقدمًا للتعامل مع هذا الأمر وهي استخدام Intents و BroadcastReceivers في Android. يكون هذا مفيدًا بشكل خاص عند الحاجة إلى إخطار العديد من المكونات بحدث التقويم. على سبيل المثال ، إذا كان الضغط على يوم في التقويم يتطلب عرض نص في Activity وملف لتنزيله بواسطة Service في الخلفية.

سيتطلب استخدام النهج السابق من Activity توفير EventHandler للمكون ، والتعامل مع الحدث ثم تمريره إلى Service . بدلاً من ذلك ، فإن وجود المكون يبث Intent كل من Activity Service عبر أجهزة استقبال BroadcastReceivers الخاصة بهم لا يجعل الحياة أسهل فحسب ، بل يساعد أيضًا في فصل Activity Service المعنية.

خاتمة

شاهد القوة الرائعة لتخصيص Android!
سقسقة

إذن ، هذه هي الطريقة التي تنشئ بها المكون المخصص الخاص بك في بضع خطوات بسيطة:

  • قم بإنشاء تخطيط XML ونمطه ليناسب احتياجاتك.
  • قم باشتقاق فئة المكون الخاصة بك من المكون الرئيسي المناسب ، وفقًا لتخطيط XML الخاص بك.
  • أضف منطق عمل المكون الخاص بك.
  • استخدم السمات لتمكين المستخدمين من تعديل سلوك المكون.
  • لتسهيل استخدام المكون في مصمم واجهة المستخدم ، استخدم وظيفة isInEditMode() في Android.

في هذه المقالة ، أنشأنا طريقة عرض التقويم كمثال ، ويرجع ذلك أساسًا إلى نقص عرض تقويم الأسهم من نواح كثيرة. لكنك لست مقيدًا بأي شكل من الأشكال بنوع المكونات التي يمكنك إنشاؤها. يمكنك استخدام نفس الأسلوب لإنشاء أي شيء تحتاجه ، والسماء هي الحد!

شكرًا لك على قراءة هذا الدليل ، أتمنى لك حظًا سعيدًا في مساعيك في البرمجة!