20 December 2014

Let's Fragment - ใส่ Indicator ให้กับ View Pager [ภาคแรก]

Updated on


        จากบทความเรื่อง Fragment ล่าสุดที่เป็นเรื่องการทำ View Pager ทีนี้มาลองเพิ่ม Indicator ให้กับ View Pager เพื่อให้ดูสมบูรณ์มากขึ้นกันต่อนะครับ

        Indicator คืออะไรหรือ?

        เป็นตัวบ่งบอกว่า View Pager ในขณะนั้นอยู่ที่ Fragment ตัวที่เท่าไร ซึ่งมีข้อดีคือไม่ต้องนั่งกำหนดทุก Fragment เพื่อบอกว่าที่แสดงอยู่ใน View Pager เป็น Fragment ตัวที่เท่าไร และยังทำให้ผู้ใช้รู้อีกด้วยว่าใน View Pager ตัวนั้นๆมีทั้งหมดกี่ Fragment

        ที่คุ้นเคยกันดีก็คือจุดกลมๆที่หน้า Homescreen ที่คอยบอกว่ากำลังแสดงหน้าไหนอยู่ จากภาพตัวอย่างข้างล่างจะเห็นว่ามีวงกลมทั้งหมด 7 วงด้วยกัน และวงกลมอันที่สองจะใหญ่กว่าชาวบ้านเค้า นั่นก็คือ View Pager ที่กำลังแสดงอยู่คือ Fragment ตัวที่ 2


        สำหรับนักพัฒนาให้เข้าใจว่า "Fragment ตัวที่เท่าไร" แต่สำหรับผู้ใช้ทั่วไปจะเรียกกันว่า "หน้าที่เท่าไร"


        ส่วนการสร้าง Indicator ให้กับ View Pager นั้นไม่ใช่เรื่องยากแต่อย่างใด เพราะว่าเจ้าของบล็อกจะใช้ Indicator Library ไปเลย เพราะว่า View Pager ส่วนใหญ่ที่มี Indicator ก็มักจะใช้ไลบรารีกัน และไลบรารียอดนิยมจะมีชื่อว่า ViewPagerIndicator ของ Jake Wharton ซึ่งผู้ที่หลงเข้ามาอ่านบางคนน่าจะคุ้นๆกับเขาคนนี้

        นาย Jake Wharton นั้นทำไลบรารีหลายอย่างมากสำหรับแอนดรอยด์ ไม่ว่าจะเป็น DiskLruCache, ActionBarSherlock, NineOldAndroids รวมไปถึง picasso, okhttp และ retrofit ของ Square เพราะว่าพี่แกทำงานอยู่ที่ Square, Inc. นั่นเอง

        ดังนั้นสิ่งที่เจ้าของบล็อกจะบอกก็คือ "ใช้ไลบรารีพี่แกเถอะ ของเค้าดีจริง แล้วชีวิตจะได้ไม่ต้องลำบาก"

        โดยการใช้งาน ViewPagerIndicator สามารถเพิ่ม Dependency เข้าไปแบบนี้ได้เลย

compile 'com.viewpagerindicator:library:2.4.1@aar'



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


        มีนอกเหนือจากนี้อีกนะเออ ลองเข้าไปดูใน GitHub ของไลบรารีตัวนี้กันได้

        สำหรับไลบรารี ViewPagerIndicator เจ้าของบล็อกคงไม่อธิบายละเอียดซักเท่าไรนัก เพราะว่าตัวไลบรารีเองก็มีตัวอย่างให้ประมาณนึงแล้ว จึงสามารถไปศึกษาต่อเองได้ไม่ยาก (เพราะใช้ง่ายมาก)

        ขอยกตัวอย่างเป็น Circle Page Indicator นะครับ เพราะค่อนข้างนิยม และนำไปใช้งานได้ยืดหยุ่นกว่า (ในแง่ความสวย) แถมไม่ต้องกำหนดขนาดเลย เพราะว่ามันจะปรับให้ตามความเหมาะสมของแต่ละหน้าจอ

        ลักษณะการใช้งานของไลบรารีตัวนี้คือจะเป็น Custom View ที่ผู้ที่หลงเข้ามาอ่านสามารถกำหนดตำแหน่งที่จะแสดงเป็น Indicator ได้ แล้วไปกำหนดในโค๊ดว่าจะให้อิงกับ View Pager ตัวไหน โดยที่ Custom View ที่มีให้เลือกใช้งานจะมีดังนี้

        • <com.viewpagerindicator.CirclePageIndicator />
        • <com.viewpagerindicator.TabPageIndicator />
        • <com.viewpagerindicator.IconPageIndicator />
        • <com.viewpagerindicator.LinePageIndicator />
        • <com.viewpagerindicator.TitlePageIndicator />
        • <com.viewpagerindicator.UnderlinePageIndicator />

        จากเดิมเจ้าของบล็อกมี Layout ของเก่า (อิงจากบทความ View Pager) ดังนี้

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f2f2f2"
    tools:context="${relativePackage}.${activityClass}" >

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/layout_menu"
        android:layout_margin="30dp"
        android:background="#ffffff" />

    <LinearLayout
        android:id="@+id/layout_menu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_centerHorizontal="true"
        android:gravity="center_horizontal" >

        <Button
            android:id="@+id/btn_prev"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Prev" />

        <Button
            android:id="@+id/btn_next"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Next" />
    </LinearLayout>

</RelativeLayout>

        ขอตัดพวก Button ออกนะครับ รวมไปถึงโค๊ด Button ที่สั่งให้เปลี่ยน Fragment ไปมาด้วย ดังนั้นก็จะเหลือแค่

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f2f2f2"
    tools:context="${relativePackage}.${activityClass}" >

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/layout_menu"
        android:layout_margin="30dp"
        android:background="#ffffff" />

</RelativeLayout>


MainActivity.java
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.ViewPager;

public class MainActivity extends FragmentActivity {
    MyPageAdapter adapter;
    ViewPager pager;
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        adapter = new MyPageAdapter(getSupportFragmentManager());
        
        pager = (ViewPager) findViewById(R.id.pager);
        pager.setAdapter(adapter);
    }
}


        เจ้าของบล็อกจะเริ่มต้นโดยเพิ่ม CirclePageIndicator ใน Layout เสียก่อน

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f2f2f2"
    tools:context="${relativePackage}.${activityClass}" >

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/layout_menu"
        android:layout_margin="30dp"
        android:background="#ffffff" />


    <com.viewpagerindicator.CirclePageIndicator
        android:id="@+id/indicator"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_alignParentTop="true"
        android:layout_margin="10dp" />

</RelativeLayout>

        ใส่ Margin เข้าไปเล็กน้อยเพื่อไม่ให้ชิดขอบบนจนเกินไป

        ที่หน้า Preview จะแสดง CirclePageIndicator ไม่ได้นะครับ ดังนั้นจะขึ้นข้อความแจ้งที่หน้า Preview แต่ไม่ต้องตกใจไป เพราะว่ามันแค่ Preview ไม่ได้เท่านั้น เมื่อทดสอบแอพฯก็จะแสดงตามปกตินั่นแหละ


        กลับมาที่ MainActivity.java ที่ต้องทำเพิ่มก็แค่ประกาศ Instance ของ CirclePageIndicator แล้วกำหนดให้สัมพันธ์กับ ViewPager ที่สร้างไว้แล้ว

MainActivity.java
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.ViewPager;
import com.viewpagerindicator.CirclePageIndicator;

public class MainActivity extends FragmentActivity {
    MyPageAdapter adapter;
    ViewPager pager;
    CirclePageIndicator indicator 
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        adapter = new MyPageAdapter(getSupportFragmentManager());
        
        pager = (ViewPager) findViewById(R.id.pager);
        pager.setAdapter(adapter);

        indicator = (CirclePageIndicator)findViewById(R.id.indicator);
        indicator .setViewPager(pager);
    }
}
        เสร็จแล้ว!! ทดสอบได้เลย

        เฮ้ย เสร็จแล้วหรอ ทำไมง่ายอย่างนี้?

        จึงเป็นสาเหตุที่เจ้าของบล็อกแนะนำให้ใช้ไลบรารีนั่นเอง



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

CirclePageIndicator indicator = (CirclePageIndicator)findViewById(R.id.indicator);
indicator .setViewPager(pager);
indicator.setSnap(true);

        คำสั่ง Snap จะทำให้ Transition ของ Indicator นั้นหายไป


        และสามารถกำหนด Properties ต่างๆผ่าน XML ได้เลย

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f2f2f2"
    tools:context="${relativePackage}.${activityClass}" >

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/layout_menu"
        android:layout_margin="30dp"
        android:background="#ffffff" />


    <com.viewpagerindicator.CirclePageIndicator
        android:id="@+id/indicator"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_alignParentTop="true"
        android:layout_margin="10dp" 
        app:radius="10dp"
        app:fillColor="#FF888888"
        app:pageColor="#88FF0000"
        app:strokeColor="#FF000000"
        app:strokeWidth="2dp" />

</RelativeLayout>
        โดยจะต้องเพิ่ม xmlns:app="http://schemas.android.com/apk/res-auto" ที่ Parent Layout ด้วยนะ ถึงจะใช้คำสั่งเหล่านั้นได้ (ไม่งั้นมันไม่รู้จัก app:xxx)

        สำหรับ Indicator บางประเภทอย่างเช่น IconPageIndecator จะต้องมีการกำหนดเพิ่มเติมใน Adapter ของ View Pager ด้วย เพื่อให้แสดงภาพแทนสัญลักษณ์ Indicator เดิม

MyPageAdapter.java
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;

import com.viewpagerindicator.IconPagerAdapter;

public class MyPageAdapter extends FragmentPagerAdapter implements IconPagerAdapter {
    private final int[] ICON_INDICATOR = { R.drawable.ic_launcher_1
                , R.drawable.ic_launcher_2
                , R.drawable.ic_launcher_3 };
    private final int NUM_ITEMS = 3;

    public MyPageAdapter(FragmentManager fm) {
        super(fm);
    }

    public int getIconResId(int index) {
        return ICON_INDICATOR[index];
    }

    public int getCount() {
        return NUM_ITEMS;
    }

    public Fragment getItem(int position) {
        if(position == 0)
            return OneFragment.newInstance();
        else if(position == 1) 
            return TwoFragment.newInstance();
        else if(position == 2) 
            return ThreeFragment.newInstance();
        return null;
    }
}

        จากตัวอย่างก็คือเจ้าของบล็อกเก็บ Drawable Resource ไว้ใน ICON_INDICATOR แล้วมีเมธอดที่ชื่อ getIconResId เพิ่มเข้ามา เพราะว่า MyPagerAdapter ได้ประกาศ Implement จากคลาส IconPagerAdapter อีกทีหนึ่ง (getIconResId เป็นเมธอดของ IconPagerAdapter) เพื่อที่จะ Return ภาพที่ต้องการแสดงเป็น Indicator ของ Fragment แต่ละตัว

        ส่วน Indicator ประเภทอื่นๆอยากให้ลองดูตัวอย่างเอา เพราะว่าไม่ได้ต่างอะไรกับ CirclePageIndicator เลย เพียงแค่เปลี่ยนชื่อคลาส มีบางคลาสที่ต้องกำหนดเพิ่มเติมเล็กน้อย อย่างเช่น IconPagerAdapter แต่คลาสส่วนใหญ่ก็จะใช้งานเหมือน CirclePagerIndicator เป๊ะๆเลย


        ไลบรารีนี่มันช่วยชีวิตจริงๆเลย XD