17 October 2016

Style and Theme - เพราะชีวิตต้องมีสไตล์

Updated on


        บนแอนดรอยด์นั้นได้มีการนำไฟล์ XML มาช่วยให้เรื่องของ Layout Design ให้ทำได้ง่ายขึ้น รวมไปถึง Resource ต่างๆอย่างการเก็บค่าข้อมูลไว้ในรูปของ XML ที่สามารถนำไปใช้งานได้ง่าย ทำอะไรได้เยอะ

        และ Style ก็เป็นหนึ่งใน Resource ที่มีประโยชน์มากๆเวลาทำเกี่ยวกับ Layout Design

        Style เป็นการเตรียม Properties ของ View หรือ ViewGroup ที่เดิมทีต้องกำหนดไว้ใน Layout XM  ไว้เป็นชุดๆเพื่อให้นำไปใช้งานได้เลย ไม่ต้องกำหนดค่าเดิมๆซ้ำซากให้กับ View ทุกๆตัว และจะช่วยจำนวนโค้ดใน Layout XML อีกด้วย

        ถ้าผู้ที่หลงเข้ามาอ่านคนใดทำพวก Web ก็จะเข้าได้ง่ายขึ้น เพราะ Style บนแอนดรอยด์นั้นก็เหมือน Style ที่เขียนด้วย CSS นั่นเอง

การสร้าง Style และเรียกใช้งาน

        สมมติว่าเจ้าของบล็อกต้องการสร้าง Layout ที่มี TextView จำนวนหลายๆตัว โดยที่แต่ละตัวก็กำหนดค่าบางอย่างเหมือนกัน ก็จะได้ Layout XML ที่มีหน้าตาประมาณนี้

...

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="20sp"
    android:textAllCaps="true"
    android:textColor="#00838f"
    android:text="@string/awesome_title" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="16sp"
    android:textColor="#263238"
    android:text="@string/awesome_description" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="16sp"
    android:textColor="#263238"
    android:text="@string/awesome_question" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="16sp"
    android:textColor="#263238"
    android:text="@string/awesome_notice" />

...

        หรือถ้าจะให้ดีหน่อยก็เก็บขนาดตัวอักษรไว้ใน dimens.xml และเก็บค่าสีไว้ใน colors.xml

...

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_title"
    android:textAllCaps="true"
    android:textColor="@color/title_text"
    android:textSize="@dimen/title_text_size" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_description"
    android:textColor="@color/body_text"
    android:textSize="@dimen/body_text_size" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_question"
    android:textColor="@color/body_text"
    android:textSize="@dimen/body_text_size" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_notice"
    android:textColor="@color/body_text"
    android:textSize="@dimen/body_text_size" />

...

        แต่เมื่อสังเกตดีๆก็จะเห็นว่า TextView เหล่านี้มีการกำหนดค่าบางอย่างซ้ำๆกัน และเป็นรูปแบบคล้ายๆกัน ซึ่งความซ้ำซ้อนตรงนี้นี่แหละที่ทำให้เจ้าของบล็อกเอา Style เข้ามาช่วยเพื่อทำให้มันง่ายขึ้น

        โดยปกติแล้ว Style จะถูกเก็บไว้ใน values/styles.xml (จริงๆแล้วจะไฟล์ไหนก็ได้ขอแค่อยู่ใน values และใช้ <style> ก็พอ...) สามารถเปิดไฟล์ดังกล่าวขึ้นมาแล้วเพิ่มหรือแก้ไข Style ตามที่ต้องการได้เลย


        พอเจ้าของบล็อกเปิดไฟล์ดังกล่าวขึ้นมาแล้วสร้าง Style ขึ้นมาโดยกำหนดชื่อว่า TitleText และ BodyText

<resources>

    <style name="TitleText">
        <item name="android:textSize">@dimen/title_text_size</item>
        <item name="android:textColor">@color/title_text</item>
        <item name="android:textAllCaps">true</item>
    </style>

    <style name="BodyText">
        <item name="android:textSize">@dimen/body_text_size</item>
        <item name="android:textColor">@color/body_text</item>
    </style>

    ...

</resources>

        แล้วก็เอา Style เหล่านี้ไปเรียกใช้งานใน TextView แทนที่ของเดิมแบบนี้

<TextView
    style="@style/TitleText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_title" />

<TextView
    style="@style/BodyText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_description" />

<TextView
    style="@style/BodyText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_question" />

<TextView
    style="@style/BodyText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/awesome_notice" />

        นั่นล่ะครับความสามารถหลักของ Style เจ้าของบล็อกไม่จำเป็นต้องมานั่งกำหนด Properties ให้กับ TextView ทุกๆตัว แต่ใช้วิธีเอาค่าเหล่านั้นไปกำหนดเป็น Style ไปเลยแล้วค่อยกำหนด Style ลงใน TextView ที่ต้องการ

Inheritance

        Style มีคุณสมบัติของ Inheritance ด้วยนะเออ (เหมือน CSS เป๊ะๆ)

<style name="BodyText">
    <item name="android:textSize">@dimen/body_text_size</item>
    <item name="android:textColor">@color/body_text_black</item>
</style>

<style name="BodyText.Gray">
    <item name="android:textColor">@color/body_text_gray</item>
</style>

<style name="BodyText.White">
    <item name="android:textColor">@color/body_text_white</item>
</style>

<style name="BodyText.Red">
    <item name="android:textColor">@color/body_text_red</item>
</style>

<style name="BodyText.Blue">
    <item name="android:textColor">@color/body_text_blue</item>
</style>

        โดย BodyText นั้นจะมีการกำหนดค่าสีและขนาดของตัวอักษรไว้ โดยที่ BodyText.Gray, BodyText.White, BodyText.Red และ BodyText.Blue จะเป็นการกำหนดค่าสีต่างๆโดยที่ขนาดของตัวอักษรยังมีเป็นค่าเดิมตามที่กำหนดไว้ใน BodyText

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

         ด้วยคุณสมบัตินี้จึงทำให้ Style มีความยืดหยุ่นในการใช้งานสูงมาก สามารถสร้าง Style ในหลายๆรูปแบบเพื่อเป็นทางเลือกในการเรียกใช้งาน

<style name="BodyText">
    <item name="android:textSize">@dimen/body_text_size</item>
    <item name="android:textColor">@color/body_text_black</item>
</style>

<style name="BodyText.Gray">
    <item name="android:textColor">@color/body_text_gray</item>
</style>

<style name="BodyText.Gray.Small">
    <item name="android:textSize">@dimen/body_text_size_small</item>
</style>

<style name="BodyText.Gray.Large">
    <item name="android:textSize">@dimen/body_text_size_large</item>
</style>

<style name="BodyText.White">
    <item name="android:textColor">@color/body_text_white</item>
</style>

<style name="BodyText.White.Small">
    <item name="android:textSize">@dimen/body_text_size_small</item>
</style>

<style name="BodyText.White.Large">
    <item name="android:textSize">@dimen/body_text_size_large</item>
</style>

...

        เวลาเรียกใช้งานใน Layout XML ก็จะขึ้น Suggest ให้เลือกแบบนี้


        สะดวกดีนะเออ

        และสมมติว่ามี Style ชุดหนึ่งอยู่แล้ว แต่ต้องการสร้างอีกชุดขึ้นมาใหม่ โดยที่ยังคงมีคุณสมบัติของอีกชุดหนึ่งก็สามารถทำได้ด้วยการกำหนด Parent ของ Style ที่ต้องการลงไป

<style name="BodyText">
    <item name="android:textSize">@dimen/body_text_size</item>
    <item name="android:textColor">@color/body_text_black</item>
</style>

<style name="CommentText" parent="BodyText">
    <item name="android:textStyle">italic</item>
</style>

      เพียงเท่านี้ CommentText ที่สร้างขึ้นมาใหม่ก็สามารถดึงคุณสมบัติเดิมของ BodyText มาได้เลย แล้วสามารถกำหนดอย่างอื่นเพิ่มเติมเข้าไปได้

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

กำหนด Style ในระดับ Application หรือ Activity ก็ได้นะ

      โดยปกติแล้ว Style จะมีผลเฉพาะ View ที่ถูกกำหนดค่าลงไป แต่ถ้าอยากให้ทั้งหน้าหรือทั้งแอปฯใช้ Style ที่เหมือนกันทั้งหมดก็สามารถกำหนดได้ใน <Application> หรือ <Activity> จากใน Android Mafenist ได้เลย

<?xml version="1.0" encoding="utf-8"?>
<manifest
    ...>

    <application
        ...
        android:theme="@style/AnyStyleAsYouWant">
        
        ...

    </application>

</manifest>

        ซึ่ง Android Dev ส่วนใหญ่คุ้นเคยกันดีกับ Style ที่ชื่อว่า AppTheme เนอะ ซึ่งเป็น Style ที่ทางแอนดรอยด์ได้เตรียมไว้ให้เพื่อให้สามารถกำหนด Style ตามต้องการได้ทันที

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

        และถ้าโปรเจคนั้นๆใช้ App Compat ของ Support v7 ก็จะกำหนด Parent เป็น Style ของ App Compat ให้โดยทันที ซึ่งเป็น Style ที่ทางแอนดรอยด์เตรียมไว้ให้เพื่อให้แอปฯมีหน้าตาเป็น Material Design ถึงแม้ว่าจะเป็นเวอร์ชันเก่านั่นเอง

        แต่ถ้าแอปฯของผู้ที่หลงเข้ามาอ่านอยากจะให้แต่ละ Activity มี Style ที่แตกต่างกัน ก็สามารถกำหนดท่ี <Activity> แยกกันแต่ละตัวได้เลย

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    ...>

    <application
        ...>
        
        <activity 
            ...
            android:theme="@style/AnyStyleAsYouWant_SetA"/>
        
        <activity 
            ...
            android:theme="@style/AnyStyleAsYouWant_SetB"/>

        ...
        
    </application>

</manifest>

        ถ้ามีการกำหนด Style ทั้งใน Application, Activity และ View ที่อยู่ข้างในนั้น จะอิงค่าตามที่กำหนดใน View ก่อน แล้วตามด้วย Activity และ Application จะอยู่ท้ายสุด

รูปแบบและข้อจำกัดในการใช้งาน Style

Style แบบ Built-In และ 3rd-Party

        Style ในแอนดรอยด์นั้นจะมีอยู่สองประเภทหลักๆคือ Build-In Style ที่ทางแอนดรอยด์ใส่มาไว้ให้ในแอนดรอยด์แต่ละเวอร์ชันกับ 3rd-Party Style ที่ไลบรารีหรือผู้ที่หลงเข้ามาอ่านสร้างขึ้นมาเพื่อใช้งานเอง

        โดย Built-In Style จะสังเกตได้ง่ายๆจากการเรียกใช้งานที่จะต้องมี android: นำหน้า

<style name="MyCustomTheme1" parent="@android:style/Theme.NoTitleBar">

    ...

</style>

<style name="MyCustomTheme2" parent="android:Theme.NoTitleBar">

    ...

</style>

        ส่วน 3rd-Party Style จะไม่มีคำว่า android: นำหน้านั่นเอง

<style name="MyCustomTheme3" parent="AwesomeTextView">

    ...

</style>

<style name="MyCustomTheme4" parent="@style/AwesomeTextView">

    ...

</style>

        จะเห็นว่า Style นั้นสามารถ เรียกใช้งานใน Parent ได้ 2 แบบ มีผลลัพธ์เหมือนกัน จะเรียกใช้งานแบบไหนก็ได้แล้วแต่สะดวกเลย

        แต่ถ้าเรียกใช้งานใน View, Activity หรือ Application จะต้องเรียกผ่าน @style/ หรือ @android:style/ เท่านั้นนะ

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="@android:style/TextAppearance.Holo.Widget.TextView"
    android:text="@string/awesome_notice" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="@style/BodyText"
    android:text="@string/awesome_notice" />

การ Inherit ที่ทำได้เฉพาะใน 3rd-Party Style

        ในกรณีที่มี Style อยู่แล้ว และต้องการ Inherit เพื่อทำเป็น Style ที่ต้องการเพิ่มเติมค่าบางอย่างลงไปก็สามารถทำได้เลย

<style name="Theme.AppCompat.Light.DarkActionBar.SleepingforLess">
        
    ...

</style>

        แต่วิธีนี้ใช้ได้กับ 3rd-Party Style เท่านั้น จะไม่สามารถใช้กับ Build-In Style ได้ ถ้าต้องการ Inherit จาก Built-In Style ให้ใช้วิธีกำหนดเป็น Parent แทน

การกำหนดค่า Attribute ใน Style

        โดยปกติแล้วการสร้าง Style ขึ้นมาซักตัวแล้วกำหนด Attribute ที่ต้องการลงไปแบบนี้

<style name="AwesomeTextView">
    <item name="android:textSize">@dimen/stupid_text_view_size</item>
    <item name="android:textColor">@color/awesome_text</item>
    <item name="android:layout_width">match_parent</item>
    <item name="android:layout_height">wrap_content</item>
</style>

        จะเห็นว่า Attribute ที่กำหนดลงไปมีคำว่า android: ด้วยทั้งหมด ทั้งนี้ก็เพราะว่า Attribute เหล่านี้เป็น Built-In Attribute ที่มีให้ใช้งานในแอนดรอยด์อยู่แล้ว

        แต่ถ้าเจ้าของบล็อกมี Custom View ตัวอื่นๆที่มี Attribute ของตัวเองแบบนี้ล่ะ?

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    ...>

    <com.akexorcist.roundcornerprogressbar.RoundCornerProgressBar
        ...
        app:rcBackgroundPadding="2dp"
        app:rcMax="100"
        app:rcProgress="50"
        app:rcRadius="0dp" />

    ...

</LinearLayout>

        เวลาสร้าง Style ให้กับ Custom View เหล่านี้ ไม่ต้องใส่อะไรนำหน้าเลย กำหนดชื่อ Attribute ลงไปใน Style ตรงๆได้เลย

<style name="AwesomeProgressBar">
    <item name="rcBackgroundPadding">2dp</item>
    <item name="rcMax">100</item>
    <item name="rcProgress">50</item>
    <item name="rcRadius">0dp</item>
</style>

Style บางตัวนั้นก็สัมพันธ์กับเวอร์ชันของแอนดรอยด์

        เพราะแอนดรอยด์เวอร์ชันใหม่ๆก็มีการ Attribute บางตัวเพิ่มเข้ามาใน View และนั่นก็หมายความว่าเวลาสร้าง Layout XML ต้องดูด้วยว่า Attribute ที่กำหนดไว้นั้นรองรับกับแอนดรอยด์เวอร์ชันขั้นต่ำที่กำหนดไว้หรือป่าว ดังนั้นถ้าเอา Attribute เหล่านี้ไปสร้างเป็น Style ก็ให้ระวังเรื่องเวอร์ชันไว้ด้วย


        จากภาพจะเห็นว่า Style ของ Material Design ที่เจ้าของบล็อกเรียกใช้นั้นรองรับเฉพาะ API 21 ขึ้นไปเท่านั้น ไม่สามารถใช้งานในเวอร์ชันต่ำกว่านั้นได้  (จึงทำให้ทีมแอนดรอยด์สร้างไลบรารี App Compat ขึ้นมาแก้ไขปัญหานี้นั่นเอง)

        ดังนั้นทางที่ดีก็ควรดูด้วยว่ามี Attribute ตัวไหนที่ไม่รองรับเวอร์ชันเก่าหรือไม่ ถ้ามีก็ให้สร้าง Style ขึ้นมาโดยแยกตามเวอร์ชันด้วย Configuration Qualifier เพื่อให้ Style รองรับเวอร์ชันที่แตกต่างกันได้


        (เวอร์ชันเก่ากว่าก็ตัด Attribute ตัวนั้นออกไปนั่นเอง)

สรุป

        Style เป็นคุณสมบัติอย่างหนึ่งของแอนดรอยด์ที่ทีมพัฒนาแอนดรอยด์ได้สร้างขึ้นมาเพื่อช่วยให้ Layout Design ทำได้ง่ายและสะดวกมากขึ้น ลด Attribute ที่ซ้ำซ้อนออกไปให้เหลือแต่เพียงชื่อ Style ที่สื่อความหมายและกระชับมากกว่า มีประโยชน์มากโดยเฉพาะอย่างยิ่งเมื่อทำงานกันเป็นทีมหรืองานที่ต้องส่งต่อให้คนอื่นรับผิดชอบ เพราะการใช้ Style จะช่วยให้นักพัฒนาสามารถ Implement ได้ทันที ไม่ต้องมานั่งก๊อป Attribute ไปแปะเองให้ครบ ทำให้ Layout Design มีรูปแบบที่เป็น Standard เหมือนกันทั้งแอปฯ

        ซึ่งสามารถใช้งานได้กับ View ทุกตัวและ Attribute ทุกแบบ แต่ก็ต้องคำนึงถึงเรื่องเวอร์ชันของแอนดรอยด์ด้วย เพราะเวอร์ชันใหม่กว่าก็จะมี Attribute ใหม่ๆเพิ่มเข้ามา จึงอาจจะทำให้ทำงานในเวอร์ชันเก่าไม่ได้ จึงควรสร้าง Style โดยแยกตามเวอร์ชันด้วย Configuration Qualifier ซะ

        และสุดท้ายฝากไว้แค่ว่า "ใช้เถอะครับ แล้วชีวิตจะไม่ลำบาก"