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

1. قبل البدء

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

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

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

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

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

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

المتطلبات

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

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

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

62ff4b2fb6c9e14a.png

3- إنشاء مظهر

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

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

20cc2cf72efef213.png

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

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