24 November 2016

มารู้จักและควบคุม System UI ใน Android App กันเถอะ

Updated on


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

        ซึ่ง System UI ที่ว่านี้ก็หมายถึง Status Bar, Navigation Bar และ Action Bar (หรือ Toolbar) ที่คุ้นเคยกันดีนั่นเอง ซึ่งระบบแอนดรอยด์ก็เปิดให้แอปฯสามารถเปลี่ยนรูปแบบการแสดงผลของ System UI ให้เหมาะกับการใช้งานแอปฯในแต่ละรูปแบบได้


        ซึ่งควบคุมยังไงได้บ้างนั้นก็จะขึ้นอยู่กับเวอร์ชันของแอนดรอยด์ด้วย เพราะเวอร์ชันเก่าๆก็อาจจะทำงานบางอย่างไม่ได้ แต่จะมีอะไรบ้างนั้นมาดูกันต่อเลย

Dimming

        หรืออีกชื่อหนึ่งที่เรียกว่า Low Profile เป็นความสามารถที่ถูกเพิ่มเข้ามาตั้งแต่สมัย Android 4.0 Ice Cream Sandwich(API 14) โดยจะลดการแสดงผลของ Status Bar และ Navigation Bar เพื่อให้ผู้ใช้สนใจเนื้อหาในแอปฯเป็นระยะเวลานานๆ



        มีข้อดีตรงที่ผู้ใช้ยังคงสามารถใช้งาน Status Bar กับ Navigation Bar ได้ทันที แต่ก็จะไม่ได้เพิ่มพื้นที่หน้าจอให้มากขึ้นเหมือนแบบ Fullscreen

        ในการสั่งงานนั้นสามารถจะใช้คำสั่งแบบนี้

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);

        คำสั่งนี้สามารถเรียกใช้งานเมื่อไรก็ได้ โดยที่ Dimming จะไม่ถูกยกเลิกจนกว่าผู้ใช้จะกดที่ Status Bar/Navigation Bar หรือใช้คำสั่งยกเลิก ซึ่งคำสั่งยกเลิก Dimming จะใช้คำสั่ง

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(0);

การซ่อน/แสดง Status Bar

        การซ่อน Status Bar นั้นทำได้ในแอนดรอยด์แทบจะทุกเวอร์ชัน แต่ทว่าจะมีความแตกต่างกันระหว่างเวอร์ชัน Android 4.1 Jelly Bean (API 16) ขึ้นไปกับเวอร์ชันที่ต่ำกว่าลงไป

เวอร์ชันต่ำกว่า Android 4.1 Jelly Bean (API 16)

        ในแอนดรอยด์เวอร์ชั้นต่ำกว่า API 16 ให้ใช้คำสั่งแบบนี้เพื่อซ่อน Status Bar

getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

        และถ้าอยากให้ Status Bar กลับมาแสดงอีกครั้งก็ให้ใช้คำสั่ง

getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);



Android 4.1 Jelly Bean (API 16) ขึ้นไป

        ส่วนกรณีที่เป็นเวอร์ชัน API 16 ขึ้นไป ให้ใช้คำสั่งซ่อน Status Bar แบบนี้แทน

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View. SYSTEM_UI_FLAG_FULLSCREEN );

        และอยากให้ Status Bar กลับมาแสดงอีกครั้งก็ให้เคลียร์ค่าเป็น 0 ซะ

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(0);


        ซึ่งจากการลองคำสั่งทั้งสองรูปแบบ จะเห็นว่าการซ่อนของ Status Bar นั้นมี Animation ที่แตกต่างกันเล็กน้อย

        แต่เพื่อให้คำสั่งยืดหยุ่นกับแต่ละเวอร์ชันก็ให้รวมคำสั่งแบบนี้แทน

// คำสั่งซ่อน Status Bar
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    View decorView = getWindow().getDecorView();
    decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
} else {
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}

// คำสั่งแสดง Status Bar
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    View decorView = getWindow().getDecorView();
    decorView.setSystemUiVisibility(0);
} else {
    getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}


        แต่ถ้าสังเกตดีๆจะเห็นว่า เมื่อมีการซ่อน/แสดง Status Bar จะทำให้ Layout ของแอปฯขยับชิดขอบบนตามด้วย ซึ่งการขยับไปมาของ Layout บางครั้งผู้ที่หลงเข้ามาอ่านอาจจะไม่ต้องการอะไรแบบนี้

        ซึ่งแอนดรอยด์สามารถกำหนดได้ว่าจะให้ Layout มีพื้นที่ที่อยู่ด้านหลัง Status Bar ด้วย เวลาที่ซ่อน/แสดง Status Bar ก็จะไม่เกิดการเปลี่ยนแปลงใดๆของ Layout เนื่องจาก Layout แสดงเต็มจออยู่ด้านหลัง Status Bar ตั้งแต่แรกอยู่แล้ว (รวมไปถึง Action Bar หรือ Toolbar ด้วย) เรียกว่า Content Behind


        จะเห็นว่าคราวนี้ Layout ที่แสดงอยู่จะไม่มีการขยับเพื่อให้ขอบชิดด้านบนเหมือนแบบเดิมอีกต่อไปแล้ว เพราะ Layout ตัวนั้นใช้พื้นที่ด้านหลัง Status Bar และ Action Bar/Toolbar ตั้งแต่แรกอยู่แล้ว

        ความสามารถนี้ใช้ได้กับแอนดรอยด์เวอร์ชัน API 16 ขึ้นไปเท่านั้น โดยใช้คำสั่ง

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

        ซึ่งคำสั่งนี้จะทำให้ Layout ใช้พื้นที่ด้านหลัง Status Bar และ Action Bar/Toolbar ด้วย

        และถ้าต้องการให้ Status Bar ทำการซ่อน/แสดง ก็ให้เปลี่ยนคำสั่งนิดหน่อย โดยให้กำหนด Flag สำหรับ Layout Fullscreen ไว้ทุกครั้ง ไม่ว่าจะแสดง Fullscreen หรือไม่

// คำสั่งซ่อน Status Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

// คำสั่งแสดง Status Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

การซ่อน/แสดง Navigation Bar

        หลังจากซ่อน/แสดง Status Bar แล้ว คราวนี้ถึงคราวของ Navigation Bar บ้าง ซึ่งการซ่อน Navigation Bar นั้นจะทำได้ตั้งแต่ API 16 ขึ้นไปเท่านั้น

// คำสั่งซ่อน Navigation Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(0);

// คำสั่งแสดง Navigation Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);


        จะเห็นว่าปุ่ม Done เจ้าของบล็อกได้จัดไว้ให้อยู่ตำแหน่งข้างล่างสุด แต่เมื่อ Navigation Bar ถูกซ่อน จึงทำให้ปุ่มต้องขยับตามไปด้วย (ปัญหาเดียวกับ Status Bar เลย)

        ถ้าโอเคแล้วก็ไม่เป็นไร แต่ถ้าทำ Content Behind กับ Navigation Bar ไปด้วยตั้งแต่แรกเลย ก็จะช่วยให้ปุ่มไม่ต้องขยับไปมาตาม Navigation Bar (แต่ก็อย่าลืมกำหนด Margin ให้กับปุ่มด้วย ไม่เช่นนั้นตำแหน่งของปุ่มจะไปซ้อนอยู่ข้างหลังของ Navigation Bar)

        ส่วนคำสั่งจะใช้คำสั่งนี้

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);

        ดังนั้นถ้าต้องการใช้ร่วมกับการซ่อน/แสดง Navigation Bar ก็จะใช้คำสั่งแบบนี้

// คำสั่งซ่อน Navigation Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);

// คำสั่งแสดง Navigation Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);

ซ่อน/แสดง Navigation Bar และ Status Bar พร้อมๆกัน

        เนื่องจากคำสั่ง setSystemUiVisibility เป็นการกำหนดค่าแบบ Flag ดังนั้นจึงกำหนดค่าหลายๆตัวพร้อมๆกันได้ จึงทำให้สามารถซ่อน/แสดง Navigation Bar และ Status Bar ไปพร้อมๆกันได้เลย

// คำสั่งซ่อน Navigation Bar และ Status Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(0);

// คำสั่งแสดง Navigation Bar และ Status Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);

        และถ้าอยากทำ Content Behind ให้กับ Navigation และ Status Bar ทั้งคู่ตั้งแต่แรกก็จะต้องใช้คำสั่งแบบนี้แทน

// คำสั่งซ่อน Navigation Bar และ Status Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | 
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);

// คำสั่งแสดง Navigation Bar และ Status Bar
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_FULLSCREEN |
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

        อย่าลืมนะครับว่าการทำ Content Behind แบบนี้จะต้องระวังว่า View บางตัวถ้าไม่ได้จัดเผื่อไว้ ก็อาจจะถูก Navigation Bar หรือ Status Bar บังได้


        จะเห็นว่าปุ่มที่อยู่ข้างล่างสุดจะแสดงให้เห็นก็ต่อเมื่อซ่อน Navigation Bar เท่านั้น ดังนั้นจัด Layout เผื่อไว้หน่อยก็ดีนะฮะ

Immersive Fullscreen

        จากที่ผ่านมาจะเห็นว่าถ้าอยากให้แอปฯสามารถแสดงผลแบบ Fullscreen ได้ ก็จะต้องเขียนโค้ดเพิ่มเข้าไปเอง ซึ่งยังไม่รวมไปถึงการออกจากโหมด Fullscreen และการทำปุ่มเพื่อกดออกจาก Fullscreen ก็อาจจะดูไม่สะดวกเสมอไป ดังนั้นบน Android 4.4 KitKat (API 19) จึงได้เพิ่มความสามารถในการแสดงผลแบบ Fullscreen ให้สะดวกและง่ายมากขึ้นโดยมีชื่อเรียกว่า Immersive Fullscreen

        ประโยชน์หลักๆของ Immersive Fullscreen คือนักพัฒนาไม่ต้องจัดการอะไรเกี่ยวกับการแสดงผลมากนัก นอกจากสั่งแค่ให้มันแสดงผลเป็นแบบ Immersive Fullscreen เท่านั้น

        โดยคำสั่งของ Immersive Fullscreen จะใช้คำสั่งแบบนี้

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | 
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_FULLSCREEN |
        View.SYSTEM_UI_FLAG_IMMERSIVE);

        เมื่อคำสั่งทำงาน แอปฯก็จะเข้าสู่ Immersive Fullscreen ทันที และในกรณีที่แอปฯนั้นๆเพิ่งจะแสดงเป็น Immersive Fullscreen ครั้งแรก จะมีหน้าต่างแสดงให้ผู้ใช้รับรู้ด้วย


        สำหรับการออกจาก Immersive Fullscreen สามารถทำได้ด้วยปัดนิ้วที่ขอบด้านบนหรือด้านล่างของหน้าจอ ก็จะออกจาก Immersive Fullscreen ทันที

        โดยการทำงานของ Immersive Fullscreen นั้นจะมีสองแบบด้วยกัน คือแบบ Non-Sticky Immersion และ Sticky Immersion

Non-Sticky Immersion

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

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | 
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_FULLSCREEN |
        View.SYSTEM_UI_FLAG_IMMERSIVE);

        เหมาะสำหรับแอปฯที่ต้องการแสดงผลแบบ Immersive Fullscreen ในการใช้งานบางอย่างเท่านั้น

Sticky Immersion

        แต่ถ้าเกิดอยากจะให้แอปฯแสดงผลแบบ Immersive Fullscreen ตลอดเวลาเลย

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | 
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
        View.SYSTEM_UI_FLAG_FULLSCREEN |
        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);

        จะเห็นว่าโค้ดนั้นคล้ายๆกับก่อนหน้านี้เลย ต่างกันแค่ตรง SYSTEM_UI_FLAG_IMMERSIVE_STICKY เท่านั้นเอง

        และการทำงานแบบ Sticky จะเป็นแบบนี้ครับ
     

        ผู้ใช้จะอยู่ในโหมด Fullscreen เสมอ เมื่อมีการลากนิ้วที่ขอบข้างบน/ล่างของหน้าจอ ก็จะเป็นการแสดง Status Bar กับ Navigation Bar และเมื่อผ่านไปซักพักก็จะซ่อนเองโดยอัตโนมัติ

        การกรณีที่ใช้ Immersive Fullscreen แบบ Sticky เจ้าของบล็อกแนะนำว่าให้ใส่โค้ดลงใน onWindowFocusChanged ได้เลย เพื่อที่ว่าจะได้เข้า Immersive Fullscreen ทันทีที่เข้าสู่หน้า Activity นั้นๆ

public class MainActivity extends Activity {

    ...

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            View decorView = getWindow().getDecorView();
            decorView.setSystemUiVisibility(
                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                            | View.SYSTEM_UI_FLAG_FULLSCREEN
                            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);}
    }
}


การซ่อน Title/Action Bar และ Status Bar ผ่าน Theme

        นอกเหนือจากการสั่งงานผ่านโค้ด Java แล้ว ยังสามารถกำหนดให้แสดงผลแบบ Fullscreen ได้ด้วยการกำหนด Theme ให้กับ Activity ที่ต้องการได้เลย

        สามารถกำหนด Theme ให้มี Parent เป็น Theme ที่เป็น Fullscreen ได้เลย


        จากภาพข้างบนก็จะเห็นว่าการสั่งให้ Fullscreen เพื่อซ่อน Status Bar นั้นจะมาพร้อมกับการซ่อน Title/Action Bar เสมอ ซึ่งก็ไม่เข้าใจเหมือนกันว่าทำไม...

        แต่ถ้าอยากจะซ่อนเฉพาะ Status Bar จริงๆก็กำหนดเพิ่มเข้าไปใน Theme ที่ใช้อยู่ได้เลย

<resources>

    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">

        <item name="android:windowFullscreen">true</item>
        
        ...

    </style>

    ...

</resources>


        เพียงเท่านี้ Theme ของผู้ที่หลงเข้ามาอ่านก็จะกลายเป็น Theme ที่ซ่อน Status Bar และผู้ใช้ก็ยังคงสามารถปัดนิ้วที่ขอบบนของจอเพื่อแสดง Status Bar ชั่วคราวได้

        แต่วิธีแบบนี้จะไม่สามารถซ่อน Navigation Bar ผ่าน Theme ได้นะ

สรุป

        การควบคุม System UI นั้นก็คือการซ่อน/แสดง Status Bar และ Navigation Bar นั่นเอง โดยสามารถทำได้หลายแบบ ขึ้นอยู่กับความต้องการใช้งาน ซึ่งจะมีเรื่องของเวอร์ชันของแอนดรอยด์ที่รองรับเป็นปัจจัยด้วย

        ซึ่งจะมีรูปแบบต่างๆดังนี้

        Dimming : ทำได้ใน API 14 ขึ้นไป เหมาะสำหรับการแสดงผลที่ไม่ได้อยากจะซ่อน Status Bar หรือ Navigation Bar แต่ว่าอยากจะให้ความดึงดูดสายตาลดลง

        Show/Hide Status Bar : คำสั่งจะขึ้นอยู่กับเวอร์ชันด้วย ซึ่งจะแบ่งที่ API 16 แต่จากที่ทดสอบดูการใช้คำสั่งเวอร์ชันต่ำกว่า API 16 ก็สามารถทำงานได้ปกติในเวอร์ชันที่ใหม่กว่า API 16 นะ

        Show/Hide Navigation Bar : ทำได้ใน API 16 ขึ้นไป

        Fullscreen : ก็แค่ใช้คำสั่งของ Status Bar และ Navigation Bar พร้อมๆกัน

        Immersive Fullscreen : แนะนำให้ใช้เมื่อต้องการแสดง Fullscreen ให้กับแอปฯที่มีเวอร์ชัน API 19 ขึ้นไป เพราะจัดการง่าย (เรียกว่าไม่ต้องทำอะไรมากนัก) มีให้เลือกด้วยว่าจะ Fullscreen ชั่วคราวหรือถาวร

        Show/Hide Status Bar with Theme : เหมาะสำหรับการซ่อน Status Bar ภายในแอปฯแบบถาวร สามารถเลือกได้ว่าจะใช้ Theme ที่มี Parent มาจาก Theme ที่เป็น Fullscreen แต่ Theme เหล่านั้นก็จะซ่อน Title/Action Bar ด้วย ถ้าอยากซ่อนแค่ Status Bar จริงๆให้เพิ่มคำสั่งเข้าไปใน Theme ของผู้ที่หลงเข้ามาอ่านโดยตรงเลยดีกว่า

        Show/Hide Navigation Bar with Theme : ทำไม่ได้จ้า


        สุดท้ายแล้วก็ขอให้เลือกใช้งานตามความเหมาะสมนะครับ