การรวมสีพื้นฐานในมุมมองต่างๆ ของ 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 เราได้สร้างบทบาทสี 4 สีสำหรับแต่ละสีที่กำหนดเอง (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 เราจะเห็นว่าการเพิ่มสีที่กำหนดเองจะแสดงแผงที่มีบทบาทของสีหลัก 4 สีในจานสีอ่อนและสีเข้ม

c6ee942b2b93cd92.png

ใน Android Views เราจะส่งออกสีเหล่านี้ให้คุณ แต่เบื้องหลังสีเหล่านี้จะแสดงด้วยอินสแตนซ์ของออบเจ็กต์ ColorRoles

คลาส ColorRoles มีพร็อพเพอร์ตี้ 4 รายการ ได้แก่ accent, onAccent, accentContainer และ onAccentContainer พร็อพเพอร์ตี้เหล่านี้คือการแสดงค่าจำนวนเต็มของสีเลขฐานสิบหกทั้ง 4 สี

public final class ColorRoles {

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

  // truncated code

}

คุณสามารถดึงบทบาทสีหลักทั้ง 4 จากสีใดก็ได้ขณะรันไทม์ได้โดยใช้ getColorRoles ในคลาส MaterialColors ที่ชื่อ getColorRoles ซึ่งจะช่วยให้คุณสร้างชุดบทบาทสีทั้ง 4 ได้ขณะรันไทม์เมื่อระบุสีเริ่มต้นที่เฉพาะเจาะจง

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

ในทำนองเดียวกัน ค่าเอาต์พุตคือค่าสีจริง ไม่ใช่ตัวชี้ไปยังค่าสี**

5. การปรับสีให้กลมกลืนคืออะไร

ระบบสีใหม่ของ Material ได้รับการออกแบบตามอัลกอริทึม ซึ่งจะสร้างสีหลัก สีรอง สีเสริม และสีกลางจากสีเริ่มต้นที่กำหนด ประเด็นหนึ่งที่เรารู้สึกกังวลและได้รับคำถามมากมายเมื่อพูดคุยกับพาร์ทเนอร์ทั้งภายในและภายนอกคือวิธีใช้สีแบบไดนามิกในขณะที่ยังคงควบคุมสีบางสีได้

สีเหล่านี้มักมีความหมายหรือบริบทที่เฉพาะเจาะจงในแอปพลิเคชัน ซึ่งจะหายไปหากมีการแทนที่ด้วยสีแบบสุ่ม หรือหากปล่อยไว้ตามเดิม สีเหล่านี้อาจดูขัดตาหรือดูไม่เข้ากัน

สีใน Material You อธิบายด้วยโทนสี โครมา และเฉดสี เฉดสีของสีหนึ่งๆ เกี่ยวข้องกับการรับรู้ของบุคคลนั้นๆ ว่าเป็นสมาชิกของช่วงสีหนึ่งๆ เทียบกับอีกช่วงสีหนึ่ง โทนสีอธิบายว่าสีนั้นดูสว่างหรือมืดเพียงใด และโครมาคือความเข้มของสี การรับรู้สีอาจได้รับผลกระทบจากปัจจัยทางวัฒนธรรมและภาษา เช่น การที่วัฒนธรรมโบราณไม่มีคำสำหรับสีน้ำเงิน ซึ่งมักมีการกล่าวถึงกันอยู่บ่อยๆ โดยสีน้ำเงินจะถูกจัดอยู่ในกลุ่มเดียวกับสีเขียว

57c46d9974c52e4a.pngเฉดสีหนึ่งๆ อาจถือเป็นสีโทนร้อนหรือโทนเย็นก็ได้ ขึ้นอยู่กับว่าเฉดสีนั้นอยู่ตรงไหนในสเปกตรัมของเฉดสี โดยทั่วไปแล้ว การเปลี่ยนไปใช้เฉดสีแดง ส้ม หรือเหลืองถือว่าเป็นการทำให้อุ่นขึ้น และการเปลี่ยนไปใช้เฉดสีน้ำเงิน เขียว หรือม่วงถือว่าเป็นการทำให้เย็นลง แม้จะอยู่ในกลุ่มสีโทนร้อนหรือเย็น คุณก็จะมีโทนสีร้อนและเย็น ด้านล่างนี้ สีเหลืองที่ "อุ่นกว่า" จะมีสีส้มมากกว่า ในขณะที่สีเหลืองที่ "เย็นกว่า" จะมีสีเขียวมากกว่า 597c6428ff6b9669.png

อัลกอริทึมการประสานสีจะตรวจสอบโทนสีของสีที่ไม่ได้เปลี่ยนและสีที่ควรประสานด้วยเพื่อค้นหาโทนสีที่กลมกลืนแต่ไม่เปลี่ยนแปลงคุณภาพของสีพื้นฐาน ในกราฟิกแรก มีการพล็อตเฉดสีเขียว เหลือง และส้มที่ไม่ค่อยกลมกลืนกันบนสเปกตรัม ในกราฟิกถัดไป สีเขียวและสีส้มได้รับการปรับให้เข้ากับเฉดสีเหลือง สีเขียวใหม่จะมีความอบอุ่นมากขึ้น ส่วนสีส้มใหม่จะมีความเย็นมากขึ้น

เฉดสีของสีส้มและสีเขียวเปลี่ยนไป แต่ก็ยังรับรู้ได้ว่าเป็นสีส้มและสีเขียว

766516c321348a7c.png

หากต้องการดูข้อมูลเพิ่มเติมเกี่ยวกับการตัดสินใจด้านการออกแบบ การสำรวจ และข้อควรพิจารณาบางอย่าง เพื่อนร่วมงานของฉันอย่าง Ayan Daniels และ Andrew Lu ได้เขียนบล็อกโพสต์ที่เจาะลึกกว่าส่วนนี้

6. การปรับสีให้กลมกลืนด้วยตนเอง

หากต้องการปรับโทนสีเดียวให้กลมกลืนกัน MaterialColors, harmonize และ harmonizeWithPrimary จะมีฟังก์ชัน 2 อย่าง

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);
  }

หากต้องการดึงชุดเสียง 4 โทน เราต้องทำอีกเล็กน้อย

เนื่องจากเรามีสีต้นฉบับอยู่แล้ว เราจึงต้องทำดังนี้

  1. พิจารณาว่าควรมีการประสานกันหรือไม่
  2. พิจารณาว่าเราอยู่ในโหมดมืดหรือไม่ และ
  3. แสดงผลออบเจ็กต์ ColorRoles ที่ปรับแล้วหรือไม่ปรับก็ได้

การพิจารณาว่าจะประสานกันหรือไม่

ในธีมที่ส่งออกจากเครื่องมือสร้างธีม Material เราได้รวมแอตทริบิวต์บูลีนโดยใช้รูปแบบการตั้งชื่อ 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 เป็นคลาส Builder ที่ช่วยให้คุณระบุสิ่งต่างๆ ที่เราทำด้วยตนเองมาจนถึงตอนนี้ได้

หลังจากเรียกบริบทปัจจุบันเพื่อให้คุณมีสิทธิ์เข้าถึงรูปแบบสีแบบไดนามิกปัจจุบันแล้ว คุณต้องระบุสีพื้นฐานที่ต้องการปรับให้กลมกลืนกัน และสร้างบริบทใหม่โดยอิงตามออบเจ็กต์ 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. การประสานแอตทริบิวต์ธีมโดยอัตโนมัติ

วิธีที่แสดงจนถึงตอนนี้อาศัยการดึงบทบาทสีจากสีแต่ละสี ซึ่งเหมาะสำหรับการแสดงว่ามีการสร้างเสียงจริง แต่ไม่สมจริงสำหรับแอปพลิเคชันที่มีอยู่ส่วนใหญ่ คุณอาจไม่ได้กำหนดสีโดยตรง แต่ใช้แอตทริบิวต์ธีมที่มีอยู่แทน

ก่อนหน้านี้ใน Codelab นี้ เราได้พูดถึงการส่งออกแอตทริบิวต์ธีม

<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 ที่มีสีที่สอดคล้องกันได้ โดยทั้ง 2 วิธีนี้มีความแตกต่างที่สำคัญ 1 อย่าง นอกจากนี้ เรายังต้องระบุธีมที่ซ้อนทับซึ่งมีช่องที่จะประสานกันด้วย

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. ตัวอย่าง UI

การกำหนดธีมเริ่มต้นและสีที่กำหนดเองโดยไม่มีการประสาน

a5a02a72aef30529.png

สีที่กำหนดเองที่สอดคล้องกัน

4ac88011173d6753.png d5084780d2c6b886.png

dd0c8b90eccd8bef.png c51f8a677b22cd54.png

12. สรุป

ในโค้ดแล็บนี้ คุณได้เรียนรู้สิ่งต่อไปนี้

  • ข้อมูลพื้นฐานเกี่ยวกับอัลกอริทึมการประสานสี
  • วิธีสร้างบทบาทสีจากสีที่เห็น
  • วิธีประสานสีในอินเทอร์เฟซผู้ใช้แบบเลือก
  • วิธีประสานชุดแอตทริบิวต์ในธีม

หากมีคำถาม โปรดถามเราได้ทุกเมื่อโดยใช้ @MaterialDesign บน Twitter

โปรดติดตามเนื้อหาการออกแบบและบทแนะนำเพิ่มเติมได้ที่ youtube.com/MaterialDesign