[ANDROID] ViewStub : Zero-sized View กับคอนเซ็ปต์ "เมื่อไรเขามา ฉันจะไป"

        ตอนที่เริ่มเขียน Android หลายๆท่านคงเคยผ่านตามาบ้างกับ Android Component ที่ชื่อว่า ViewStub และผมก็เป็นหนึ่งในนั้นที่ได้แค่ผ่านๆตา แต่ไม่เคยลองใช้งาน จนวันนึงได้ไปฟังเรื่อง Advance Android Layout Walk Through โดยคุณเอก GDE ที่งาน Code Mania 11 (สำหรับท่านที่ไม่ได้ไปสามารถดูย้อนหลังได้ที่ VDO และ สไลด์) ซึ่งมีการพูดถึง ViewStub พอได้ฟังแล้วก็เห็นถึงความเจ๋งของมัน วันนี้ก็เลยหยิบเอามาเขียนซักเล็กน้อยครับ


ViewStub คืออะไร ?

        ViewStub คือ View ชนิดหนึ่ง มีการใช้งาน Memory น้อยมากกกกก (Lightweight View) เนื่องจากไม่มีการเขียนสิ่งไดๆลงบนจอเลย(Zero-sized View) ด้วยเหตุนี้ ViewStub จึงไม่ปรากฏบนจอ(Invisible)
        โดยที่ไม้เด็ดของ ViewStub คือ สามารถ Inflate View อื่นๆ เข้ามาแทนที่ตัวมันเอง ในขณะ Runtime ได้ เพียงแค่เรียกใช้คำสั่ง  viewStub.inflate()  หรือ  viewStub.setVisibility(int)  ซึ่งเป็นที่มาของหัวข้อเรื่องที่ว่า "เมื่อไรเขามา ฉันจะไป" นั่นเอง

ใช้งานเมื่อไร ?

        ViewStub เหมาะสำหรับการใช้งานกับ View ที่ไม่ได้แสดงผลทุกครั้งในหน้าเดิมๆ หรือ View ที่นานๆทีจะแสดงผลสักครั้งนึง เช่น แถบคาดว่า User ถูกแบน, Progress bar แสดง % ในการ Download ข้อมูล เป็นต้น
        เนื่องจากว่า ViewStub ไม่มีการแสดงผลลงบนจอ และมี View Hierarchy เพียงชั้นเดียว จึงทำให้ใช้งาน Memory น้อยกว่ามาก เมื่อเทียบกับการที่ใส่ View ลงไปตรงๆใน XML แล้วสั่ง visibility="gone"(ลักไก่สุดๆ) เพราะถึงแม้ว่าจะซ่อนไปแล้ว View นั้นๆก็ยังจะโดน Inflate อยู่ดี ซึ่งก็หมายถึง Memory ที่ต้องเสียไปโดยเปล่าประโยชน์ นั่นเอง :(

การใช้งาน

หน้าตาของ ViewStub ในไฟล์ .xml
<ViewStub android:id="@+id/stub"
          android:inflatedId="@+id/layout_banned_user"
          android:layout="@layout/banned_user"
          android:layout_width="match_parent"
          android:layout_height="wrap_content" />
โดยที่ android:inflatedId คือ การกำหนด Id ให้กับ View ที่ Inflate มาแทน ViewStub
          android:layout คือ ไฟล์ layout .xml ที่ต้องการให้มาแทน ViewStub

และเมื่อต้องการให้ View อื่นมาแทนที่ ViewStub ก็เพียงเรียกคำสั่ง inflate() หรือ setVisibility(int) ดังตัวอย่าง
ViewStub viewStub = (ViewStub) findViewById(R.id.stub);

viewStub.setVisibility(View.VISIBLE);
// หรือ
View layoutBannedUser = viewStub.inflate();
        การใช้งาน ViewStub ก็มีเพียงเท่านี้ แต่บางท่านอาจจะยังงงๆอยู่ เพื่อความเข้าใจมากขึ้นเราลองมาทำ Work Shop กันดีกว่าครับ :)

ลองทำ Work Shop ง่ายๆกันดีกว่า

                                                        A                                              B
โจทย์คือ
 - หาก User อยู่ในสถานะปกติ ก็ให้แสดง Avatar ของ User (A)
 - แต่หาก User ถูกแบน ให้แสดง Overlay สีดำทับพร้อมกับแสดงข้อความ (B)

1. สร้าง Layout ของ User Avatar

ขอตั้งชื่อว่า activity_user.xml ก็จะได้ผลลัพธ์จะได้ดังรูป (A) ครับ
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageview_avatar"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_centerInParent="true"
        android:src="@drawable/avatar"/>
</RelativeLayout>

2. ลองเพิ่ม Layout ของ Overlay สีดำและข้อความลงไปใน Layout เดิม

ผลลัพธ์จะได้ดังรูป (B)
<RelativeLayout>
    <ImageView />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#CC000000"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            style="@style/Base.TextAppearance.AppCompat.Large.Inverse"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="YOU'VE BEEN BANNED"/>

        <TextView
            style="@style/TextAppearance.AppCompat.Medium.Inverse"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Please contact administrator"/>
    </LinearLayout>
</RelativeLayout>
        จริงๆ แล้วทำเพียงเท่านี้ก็เพียงพอต่อความต้องการของโจทย์แล้ว แต่เดี๋ยวก่อน Layout Overlay สีดำไม่ได้แสดงทุกครั้งใช่ไหมครับ :) แล้วเราจะทำยังไงหละ ?? ....... ถูกต้องนะคร้าบบบ ViewStub ช่วยท่านได้ แล้วทำยังไงหละ ?? ลองมาดูกันดีกว่า

3. ย้าย Layout ของ Overlay สีดำและข้อความ ไปไว้อีกไฟล์นึง

ขอตั้งชื่อว่า banned_user.xml ละกันนะครับ
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#CC000000"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        style="@style/Base.TextAppearance.AppCompat.Large.Inverse"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="YOU'VE BEEN BANNED"/>

    <TextView
        style="@style/TextAppearance.AppCompat.Medium.Inverse"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Please contact administrator"/>
</LinearLayout>
*** หมายเหตุ : View ที่จะ Inflate เข้ามาแทน ViewStub ไม่รองรับการใช้งาน tag <merge />  นะครับ

4. กลับไปที่ activity_user.xml เพื่อเพิ่ม ViewStub

กำหนด View ที่จะมาแทน ViewStub ด้วย tag android:layout="@layout/banned_user"
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageview_avatar"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_centerInParent="true"
        android:src="@drawable/avatar"/>

    <!-- เพิ่ม ViewStub -->
    <ViewStub
        android:id="@+id/stub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:inflatedId="@+id/layout_banned_user"
        android:layout="@layout/banned_user"/>
</RelativeLayout>

5. เมื่อ User ถูกแบน ให้เรียกใช้คำสั่ง ViewStuff.inflate()

public class UserActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        .
        .

        if(isBanned()) {
            ViewStub viewStub = (ViewStub) findViewById(R.id.stub);
            viewStub.inflate();
        }
    }
} 
        เพียงเท่านี้ก็จะได้ผลลัพธ์ตามที่โจทย์ต้องการ พร้อมทั้งยังได้ Application ที่ประสิทธิภาพอีกด้วยนะครับ

แอบดูเบื้องหลัง

        ลองมาดู View Hierarchy ระหว่างตอนที่ยังเป็น ViewStub (C) และหลังจาก Inflate View (D) อื่นเข้ามาแทน กันหน่อย
C

 D

        จะเห็นได้ว่า android:inflatedId ที่กำหนดไว้ด้านบน ถูกกำหนดเป็น ID ให้กับ View ที่ถูก Inflate เข้ามาแทน ViewStub นั่นเอง และสำหรับเรื่อง View Hierarchy คงไม่จำเป็นต้องพูดอะไรแล้วเนาะ เพราะจากภาพด้านบนคงจะพอบอกได้แล้วว่า ViewStub ดียังไง
        สำหรับเรื่องของ ViewStub คงจบลงเท่านี้ สุดท้ายหากมีข้อสงสัยหรือแนะนำก็ฝากไว้ใน Comment ได้เลยนะครับ ขอบคุณครับ

ปล. ดูโค้ดทั้งหมดได้ที่ github.com

ลิงค์อ้างอิง

http://developer.android.com/reference/android/view/ViewStub.html
http://developer.android.com/training/improving-layouts/loading-ondemand.html
http://android-developers.blogspot.in/2009/03/android-layout-tricks-3-optimize-with.html

Teeranai P

Developer ตัวน้อยๆ ที่หลงใหลในโลกของการพัฒนา Software. รักการเขียนโปรแกรมเป็นอันดับ 2 รองลงมาจากการนอน