تنسيق الألوان الأساسي في طرق عرض Android

1. قبل البدء

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية تنسيق ألوانك المخصّصة مع الألوان التي ينشئها مظهر ديناميكي.

المتطلبات الأساسية

على المطوّرين:

  • أن تكون على دراية بمفاهيم السمات الأساسية في Android
  • التعامل بشكل مريح مع طرق عرض التطبيقات المصغّرة على Android وخصائصها

أهداف الدورة التعليمية

  • كيفية استخدام ميزة "مواءمة الألوان" في تطبيقك باستخدام طرق متعدّدة
  • طريقة عمل ميزة "مواءمة الألوان" وكيفية تغيير الألوان

المتطلبات

  • جهاز كمبيوتر مثبّت عليه نظام التشغيل Android إذا كنت تريد اتّباع الخطوات.

2. نظرة عامة على التطبيق

‫Voyaĝi هو تطبيق نقل عام يستخدم مظهرًا ديناميكيًا. في العديد من أنظمة النقل العام، يشكّل اللون مؤشرًا مهمًا للحافلات أو القطارات أو الترام، ولا يمكن استبداله بأي ألوان أساسية أو ثانوية أو من الدرجة الثالثة متوفرة. سنركّز جهودنا على RecyclerView لبطاقات النقل العام الملوّنة.

62ff4b2fb6c9e14a.png

3- إنشاء مظهر

ننصحك باستخدام أداتنا Material Theme Builder كخطوة أولى لإنشاء تصميم Material3. في علامة التبويب "مخصّص"، يمكنك الآن إضافة المزيد من الألوان إلى المظهر. على اليسار، سيتم عرض أدوار الألوان ولوحات الألوان الدرجات اللونية لتلك الألوان.

في قسم الألوان الموسّعة، يمكنك إزالة الألوان أو إعادة تسميتها.

20cc2cf72efef213.png

ستعرض قائمة التصدير عددًا من خيارات التصدير الممكنة. في وقت كتابة هذا المستند، لا تتوفّر المعالجة الخاصة لإعدادات التوافق في أداة Material Theme Builder إلا في Android Views.

6c962ad528c09b4.png

فهم قيم التصدير الجديدة

للسماح لك باستخدام هذه الألوان وأدوار الألوان المرتبطة بها في سماتك سواء اخترت التنسيق أو لا، يتضمّن التنزيل الذي تم تصديره الآن ملف attrs.xml يحتوي على أسماء أدوار الألوان لكل لون مخصّص.

<resources>
   <attr name="colorCustom1" format="color" />
   <attr name="colorOnCustom1" format="color" />
   <attr name="colorCustom1Container" format="color" />
   <attr name="colorOnCustom1Container" format="color" />
   <attr name="harmonizeCustom1" format="boolean" />

   <attr name="colorCustom2" format="color" />
   <attr name="colorOnCustom2" format="color" />
   <attr name="colorCustom2Container" format="color" />
   <attr name="colorOnCustom2Container" format="color" />
   <attr name="harmonizeCustom2" format="boolean" />
</resources>

في ملف themes.xml، أنشأنا أدوار الألوان الأربعة لكل لون مخصّص (color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>). تعكس خصائص harmonize<name> ما إذا كان المطوّر قد اختار الخيار في أداة Material Theme Builder. لن يتم تغيير اللون في المظهر الأساسي.

<resources>
   <style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
       <!--- Normal theme attributes ... -->

       <item name="colorCustom1">#006876</item>
       <item name="colorOnCustom1">#ffffff</item>
       <item name="colorCustom1Container">#97f0ff</item>
       <item name="colorOnCustom1Container">#001f24</item>
       <item name="harmonizeCustom1">false</item>

       <item name="colorCustom2">#016e00</item>
       <item name="colorOnCustom2">#ffffff</item>
       <item name="colorCustom2Container">#78ff57</item>
       <item name="colorOnCustom2Container">#002200</item>
       <item name="harmonizeCustom2">false</item>
   </style>
</resources>

في ملف colors.xml، يتم تحديد الألوان الأساسية المستخدَمة لإنشاء أدوار الألوان المذكورة أعلاه، بالإضافة إلى القيم المنطقية لتحديد ما إذا كان سيتم تغيير لوحة الألوان أم لا.

<resources>
   <!-- other colors used in theme -->

   <color name="custom1">#1AC9E0</color>
   <color name="custom2">#32D312</color>
</resources>

4. فحص اللون المخصّص

عند تكبير اللوحة الجانبية في أداة Material Theme Builder، نرى أنّ إضافة لون مخصّص تعرض لوحة تتضمّن أدوار الألوان الأساسية الأربعة في لوحة ألوان فاتحة وداكنة.

c6ee942b2b93cd92.png

في "طرق العرض في Android"، نصدّر لك هذه الألوان، ولكن يمكن تمثيلها في الخلفية بمثيل من العنصر ColorRoles.

يحتوي صف ColorRoles على أربع سمات، هي accent وonAccent وaccentContainer وonAccentContainer. هذه الخصائص هي التمثيل العددي للألوان السداسية العشرية الأربعة.

public final class ColorRoles {

  private final int accent;
  private final int onAccent;
  private final int accentContainer;
  private final int onAccentContainer;

  // truncated code

}

يمكنك استرداد أدوار الألوان الرئيسية الأربعة من لون عشوائي في وقت التشغيل باستخدام getColorRoles في فئة MaterialColors المسماة getColorRoles التي تتيح لك إنشاء مجموعة أدوار الألوان الأربعة هذه في وقت التشغيل مع لون أساسي محدّد.

public static ColorRoles getColorRoles(
    @NonNull Context context,
    @ColorInt int color
) { /* implementation */ }

وبالمثل، فإنّ قيم الإخراج هي قيم الألوان الفعلية، وليست مؤشرات إليها.**

5- ما المقصود بمواءمة الألوان؟

نظام الألوان الجديد في Material مصمَّم بشكل خوارزمي، إذ يتيح إنشاء ألوان أساسية وثانوية وثالثية ومحايدة من لون أساسي محدّد. أحد الجوانب التي أثارت قلقًا كبيرًا لدى الشركاء الداخليين والخارجيين هو كيفية الاستفادة من ميزة "الألوان الديناميكية" مع الحفاظ على التحكّم في بعض الألوان.

وغالبًا ما تحمل هذه الألوان معنى أو سياقًا معيّنًا في التطبيق، وسيتم فقدان هذا المعنى أو السياق إذا تم استبدالها بلون عشوائي. بدلاً من ذلك، إذا تم ترك هذه الألوان كما هي، قد تبدو غير متناسقة أو غير مناسبة.

يتم وصف اللون في Material You من خلال درجة اللون والصفاء والظل. ترتبط صبغة اللون بإدراك الشخص له كعنصر من نطاق ألوان معيّن مقارنةً بنطاق آخر. تصف الدرجة مدى سطوع اللون أو شحوبه، بينما تشير الصفاء إلى كثافة اللون. يمكن أن تتأثر درجة اللون بعوامل ثقافية ولغوية، مثل عدم توفّر كلمة للون الأزرق في الحضارات القديمة، حيث كان يُصنّف ضمن فئة اللون الأخضر.

57c46d9974c52e4a.pngيمكن اعتبار درجة لون معيّنة دافئة أو باردة استنادًا إلى موضعها في طيف درجات الألوان. ويُعتقد عمومًا أنّ التحوّل إلى درجة لون حمراء أو برتقالية أو صفراء يجعل الصورة أكثر دفئًا، بينما التحوّل إلى درجة لون زرقاء أو خضراء أو أرجوانية يجعلها أكثر برودة. حتى ضمن الألوان الدافئة أو الباردة، ستتضمّن درجات دافئة وباردة. في ما يلي، يميل اللون الأصفر "الأكثر دفئًا" إلى اللون البرتقالي، بينما يميل اللون الأصفر "الأكثر برودة" إلى اللون الأخضر. 597c6428ff6b9669.png

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

تغيّر اللون في البرتقالي والأخضر، ولكن لا يزال من الممكن إدراكهما على أنّهما برتقالي وأخضر.

766516c321348a7c.png

إذا أردت معرفة المزيد عن بعض قرارات التصميم والاستكشافات والاعتبارات، كتب زميليّ "أيان دانيالز" و"أندرو لو" منشور مدونة تتضمّن تفاصيل أكثر من هذا القسم.

6. تنسيق لون يدويًا

لمواءمة نغمة واحدة، هناك دالتان في MaterialColors وharmonize وharmonizeWithPrimary.

يستخدم harmonizeWithPrimary Context كوسيلة للوصول إلى المظهر الحالي، وبالتالي إلى اللون الأساسي منه.

@ColorInt
public static int harmonizeWithPrimary(@NonNull Context context, @ColorInt int colorToHarmonize) {
    return harmonize(
        colorToHarmonize,
        getColor(context, R.attr.colorPrimary, MaterialColors.class.getCanonicalName()));
  }


@ColorInt
  public static int harmonize(@ColorInt int colorToHarmonize, @ColorInt int colorToHarmonizeWith) {
    return Blend.harmonize(colorToHarmonize, colorToHarmonizeWith);
  }

لاسترداد مجموعة النغمات الأربع، علينا إجراء بعض الخطوات الإضافية.

بما أنّ لدينا لون المصدر، علينا إجراء ما يلي:

  1. تحديد ما إذا كان يجب تنسيقه
  2. تحديد ما إذا كنا في الوضع الداكن
  3. إرجاع عنصر ColorRoles متوافق أو غير متوافق

تحديد ما إذا كان يجب التوفيق بين البيانات

في المظهر الذي تم تصديره من Material Theme Builder، أدرجنا سمات منطقية باستخدام التسمية harmonize<Color>. في ما يلي دالة ملائمة للوصول إلى هذه القيمة.

إذا تم العثور على قيمة، يتم عرضها، وإلا يتم تحديد أنّه لا يجب تنسيق اللون.

// Looks for associated harmonization attribute based on the color id
// custom1 ===> harmonizeCustom1
fun shouldHarmonize(context: Context, colorId: Int): Boolean {
   val root = context.resources.getResourceEntryName(colorId)
   val harmonizedId = "harmonize" + root.replaceFirstChar { it.uppercaseChar() }
   
   val identifier = context.resources.getIdentifier(
           harmonizedId, "bool", context.packageName)
   
   return if (identifier != 0) context.resources.getBoolean(identifier) else false
}

إنشاء كائن ColorRoles متوافق

retrieveHarmonizedColorRoles هي دالة ملائمة أخرى تجمع كل الخطوات المذكورة أعلاه: استرداد قيمة اللون لمورد مُسمّى، ومحاولة مطابقة سمة منطقية لتحديد التوافق، وعرض عنصر ColorRoles مشتق من اللون الأصلي أو الممزوج (بالنظر إلى نظام الألوان الفاتح أو الداكن).

fun retrieveHarmonizedColorRoles(
   view: View,
   customId: Int,
   isLight: Boolean
): ColorRoles {
   val context = view.context
   val custom = context.getColor(customId);
  
   val shouldHarmonize = shouldHarmonize(context, customId)
   if (shouldHarmonize) {
       val blended = MaterialColors.harmonizeWithPrimary(context, custom)
       return MaterialColors.getColorRoles(blended, isLight)
   } else return MaterialColors.getColorRoles(custom, isLight)
}

7. تعبئة بطاقات النقل العام

كما ذكرنا سابقًا، سنستخدم RecyclerView ومحوّلًا لتعبئة مجموعة بطاقات النقل العام وتلوينها.

e4555089b065b5a7.png

تخزين بيانات النقل العام

لتخزين بيانات النص ومعلومات الألوان لبطاقات النقل العام، نستخدم فئة بيانات تخزّن الاسم والوجهة ومعرّف مورد اللون.

data class TransitInfo(val name: String, val destination: String, val colorId: Int)

/*  truncated code */

val transitItems = listOf(
   TransitInfo("53", "Irvine", R.color.custom1),
   TransitInfo("153", "Brea", R.color.custom1),
   TransitInfo("Orange County Line", "Oceanside", R.color.custom2),
   TransitInfo("Pacific Surfliner", "San Diego", R.color.custom2)
)

سنستخدم هذا اللون لإنشاء النغمات التي نحتاجها في الوقت الفعلي.

يمكنك المزامنة في وقت التشغيل باستخدام الدالة onBindViewHolder التالية.

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   val transitInfo = list.get(position)
   val color = transitInfo.colorId
   if (!colorRolesMap.containsKey(color)) {

       val roles = retrieveHarmonizedColorRoles(
           holder.itemView, color,
           !isNightMode(holder.itemView.context)
       )
       colorRolesMap.put(color, roles)
   }

   val card = holder.card
   holder.transitName.text = transitInfo.name
   holder.transitDestination.text = transitInfo.destination

   val colorRoles = colorRolesMap.get(color)
   if (colorRoles != null) {
       holder.card.setCardBackgroundColor(colorRoles.accentContainer)
       holder.transitName.setTextColor(colorRoles.onAccentContainer)
       holder.transitDestination.setTextColor(colorRoles.onAccentContainer)
   }
}

8. مواءمة الألوان تلقائيًا

بدلاً من التعامل مع عملية التوافق يدويًا، يمكنك الاستعانة بخدمة تتولّى هذه العملية نيابةً عنك. ‫HarmonizedColorOptions هي فئة إنشاء تتيح لك تحديد معظم ما فعلناه حتى الآن يدويًا.

بعد استرداد السياق الحالي لتتمكّن من الوصول إلى نظام الألوان الديناميكي الحالي، عليك تحديد الألوان الأساسية التي تريد تنسيقها وإنشاء سياق جديد استنادًا إلى عنصر HarmonizedColorOptions هذا والسياق المفعَّل DynamicColors.

إذا كنت لا تريد تنسيق لون، ما عليك سوى عدم تضمينه في harmonizedOptions.

val newContext = DynamicColors.wrapContextIfAvailable(requireContext())


val harmonizedOptions = HarmonizedColorsOptions.Builder()
 .setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
 .build();

harmonizedContext =
 HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)

بعد ضبط لون الخلفية الأساسي الموحّد، يمكنك تعديل onBindViewHolder لاستدعاء MaterialColors.getColorRoles وتحديد ما إذا كان يجب أن تكون الأدوار المعروضة فاتحة أو داكنة.

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   /*...*/
   val color = transitInfo.colorId
   if (!colorRolesMap.containsKey(color)) {

       val roles = MaterialColors.getColorRoles(context.getColor(color), !isNightMode(context))

       )
       colorRolesMap.put(color, roles)
   }

   val card = holder.card
   holder.transitName.text = transitInfo.name
   holder.transitDestination.text = transitInfo.destination

   val colorRoles = colorRolesMap.get(color)
   if (colorRoles != null) {
       holder.card.setCardBackgroundColor(colorRoles.accentContainer)
       holder.transitName.setTextColor(colorRoles.onAccentContainer)
       holder.transitDestination.setTextColor(colorRoles.onAccentContainer)
   }
}

9- مواءمة سمات المظهر تلقائيًا

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

في وقت سابق من هذا الدرس العملي، تحدّثنا عن تصدير سمات المظهر.

<resources>
   <style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
       <!--- Normal theme attributes ... -->

       <item name="colorCustom1">#006876</item>
       <item name="colorOnCustom1">#ffffff</item>
       <item name="colorCustom1Container">#97f0ff</item>
       <item name="colorOnCustom1Container">#001f24</item>
       <item name="harmonizeCustom1">false</item>

       <item name="colorCustom2">#016e00</item>
       <item name="colorOnCustom2">#ffffff</item>
       <item name="colorCustom2Container">#78ff57</item>
       <item name="colorOnCustom2Container">#002200</item>
       <item name="harmonizeCustom2">false</item>
   </style>
</resources>

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

val dynamicColorsContext = DynamicColors.wrapContextIfAvailable(requireContext())

// Harmonizing individual attributes
val harmonizedColorAttributes = HarmonizedColorAttributes.create(
 intArrayOf(
   R.attr.colorCustom1,
   R.attr.colorOnCustom1,
   R.attr.colorCustom1Container,
   R.attr.colorOnCustom1Container,
   R.attr.colorCustom2,
   R.attr.colorOnCustom2,
   R.attr.colorCustom2Container,
   R.attr.colorOnCustom2Container
 ), R.style.AppTheme_Overlay
)
val harmonizedOptions =
 HarmonizedColorsOptions.Builder().setColorAttributes(harmonizedColorAttributes).build()

val harmonizedContext =
 HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)

سيستخدم المحوّل السياق الموحّد. يجب أن تشير القيم في تراكب السمة إلى الإصدار الفاتح أو الداكن غير المتوافق.

<style name="AppTheme.Overlay" parent="AppTheme">
   <item name="colorCustom1">@color/harmonized_colorCustom1</item>
   <item name="colorOnCustom1">@color/harmonized_colorOnCustom1</item>
   <item name="colorCustom1Container">@color/harmonized_colorCustom1Container</item>
   <item name="colorOnCustom1Container">@color/harmonized_colorOnCustom1Container</item>

   <item name="colorCustom2">@color/harmonized_colorCustom2</item>
   <item name="colorOnCustom2">@color/harmonized_colorOnCustom2</item>
   <item name="colorCustom2Container">@color/harmonized_colorCustom2Container</item>
   <item name="colorOnCustom2Container">@color/harmonized_colorOnCustom2Container</item>
</style>

داخل ملف تنسيق XML، يمكننا استخدام هذه السمات المتوافقة كالمعتاد.

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   style="?attr/materialCardViewFilledStyle"
   android:id="@+id/card"
   android:layout_width="80dp"
   android:layout_height="100dp"
   android:layout_marginStart="8dp"
   app:cardBackgroundColor="?attr/colorCustom1Container"
   >

   <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_margin="8dp">

       <TextView
           android:id="@+id/transitName"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:textSize="28sp"
           android:textStyle="bold"
           android:textColor="?attr/colorOnCustom1Container"
           app:layout_constraintTop_toTopOf="parent" />

       <TextView
           android:id="@+id/transitDestination"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginBottom="4dp"
           android:textColor="?attr/colorOnCustom1Container"
           app:layout_constraintBottom_toBottomOf="parent" />
   </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

10. رمز المصدر

package com.example.voyagi.harmonization.ui.dashboard

import android.content.Context
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.voyagi.harmonization.R
import com.example.voyagi.harmonization.databinding.FragmentDashboardBinding
import com.example.voyagi.harmonization.ui.home.TransitCardAdapter
import com.example.voyagi.harmonization.ui.home.TransitInfo
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.ColorRoles
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.HarmonizedColorAttributes
import com.google.android.material.color.HarmonizedColors
import com.google.android.material.color.HarmonizedColorsOptions
import com.google.android.material.color.MaterialColors


class DashboardFragment : Fragment() {

 enum class TransitMode { BUS, TRAIN }
 data class TransitInfo2(val name: String, val destination: String, val mode: TransitMode)

 private lateinit var dashboardViewModel: DashboardViewModel
 private var _binding: FragmentDashboardBinding? = null

 // This property is only valid between onCreateView and
 // onDestroyView.
 private val binding get() = _binding!!

 override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
 ): View? {
   dashboardViewModel =
     ViewModelProvider(this).get(DashboardViewModel::class.java)

   _binding = FragmentDashboardBinding.inflate(inflater, container, false)
   val root: View = binding.root


   val recyclerView = binding.recyclerView

   val transitItems = listOf(
     TransitInfo2("53", "Irvine", TransitMode.BUS),
     TransitInfo2("153", "Brea", TransitMode.BUS),
     TransitInfo2("Orange County Line", "Oceanside", TransitMode.TRAIN),
     TransitInfo2("Pacific Surfliner", "San Diego", TransitMode.TRAIN)
   )
  
   val dynamicColorsContext = DynamicColors.wrapContextIfAvailable(requireContext())

   // Harmonizing individual attributes
   val harmonizedColorAttributes = HarmonizedColorAttributes.create(
     intArrayOf(
       R.attr.colorCustom1,
       R.attr.colorOnCustom1,
       R.attr.colorCustom1Container,
       R.attr.colorOnCustom1Container,
       R.attr.colorCustom2,
       R.attr.colorOnCustom2,
       R.attr.colorCustom2Container,
       R.attr.colorOnCustom2Container
     ), R.style.AppTheme_Overlay
   )
   val harmonizedOptions =
     HarmonizedColorsOptions.Builder().setColorAttributes(harmonizedColorAttributes).build()

   val harmonizedContext =
     HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)


   val adapter = TransitCardAdapterAttr(transitItems, harmonizedContext)
   recyclerView.adapter = adapter
   recyclerView.layoutManager =
     LinearLayoutManager(harmonizedContext, RecyclerView.HORIZONTAL, false)

   return root
 }

 override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
 }
}

class TransitCardAdapterAttr(val list: List<DashboardFragment.TransitInfo2>, context: Context) :
 RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 val colorRolesMap = mutableMapOf<Int, ColorRoles>()
 private var harmonizedContext: Context? = context

 override fun onCreateViewHolder(
   parent: ViewGroup,
   viewType: Int
 ): RecyclerView.ViewHolder {
   return if (viewType == DashboardFragment.TransitMode.BUS.ordinal) {
     BusViewHolder(LayoutInflater.from(harmonizedContext).inflate(R.layout.transit_item_bus, parent, false))
   } else TrainViewHolder(LayoutInflater.from(harmonizedContext).inflate(R.layout.transit_item_train, parent, false))
 }

 override fun getItemCount(): Int {
   return list.size
 }

 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
   val item = list[position]
   if (item.mode.ordinal == DashboardFragment.TransitMode.BUS.ordinal) {
     (holder as BusViewHolder).bind(item)
     (holder as TransitBindable).adjustNameLength()
   } else {
       (holder as TrainViewHolder).bind(item)
       (holder as TransitBindable).adjustNameLength()
   }
 }

 override fun getItemViewType(position: Int): Int {
   return list[position].mode.ordinal
 }

 interface TransitBindable {
   val card: MaterialCardView
   var transitName: TextView
   var transitDestination: TextView

   fun bind(item: DashboardFragment.TransitInfo2) {
     transitName.text = item.name
     transitDestination.text = item.destination
   }
   fun Float.toDp(context: Context) =
     TypedValue.applyDimension(
       TypedValue.COMPLEX_UNIT_DIP,
       this,
       context.resources.displayMetrics
     )
   fun adjustNameLength(){
     if (transitName.length() > 4) {
       val layoutParams = card.layoutParams
       layoutParams.width = 100f.toDp(card.context).toInt()
       card.layoutParams = layoutParams
       transitName.setTypeface(Typeface.DEFAULT_BOLD);

       transitName.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f)
     }
   }
 }

 inner class BusViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), TransitBindable {
   override val card: MaterialCardView = itemView.findViewById(R.id.card)
   override var transitName: TextView = itemView.findViewById(R.id.transitName)
   override var transitDestination: TextView = itemView.findViewById(R.id.transitDestination)
 }
 inner class TrainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), TransitBindable {
   override val card: MaterialCardView = itemView.findViewById(R.id.card)
   override var transitName: TextView = itemView.findViewById(R.id.transitName)
   override var transitDestination: TextView = itemView.findViewById(R.id.transitDestination)
 }
}

11. أمثلة على واجهات المستخدم

استخدام المظهر التلقائي والألوان المخصّصة بدون مواءمة

a5a02a72aef30529.png

ألوان مخصّصة متناسقة

4ac88011173d6753.png d5084780d2c6b886.png

dd0c8b90eccd8bef.png c51f8a677b22cd54.png

12. ملخّص

في هذا الدرس التطبيقي، تعلّمت ما يلي:

  • أساسيات خوارزمية مواءمة الألوان
  • كيفية إنشاء أدوار الألوان من لون مرئي معيّن
  • كيفية تنسيق لون بشكل انتقائي في واجهة مستخدم
  • كيفية تنسيق مجموعة من السمات في مظهر

إذا كانت لديك أي أسئلة، يمكنك طرحها علينا في أي وقت باستخدام @MaterialDesign على Twitter.

يمكنك متابعة المزيد من المحتوى والبرامج التعليمية حول التصميم على youtube.com/MaterialDesign