Let’s Fragment — มาทำ View Pager กันเถิดพี่น้อง~ [ตอนที่ 1]
อยู่ในระหว่างการปรับปรุง
เฮ้~! ในที่สุดก็มาถึงบทความเรื่องนี้เสียที หลังจากที่เกริ่นกล่าวไว้มาหลายบทความแล้ว ณ ตอนนี้ก็ถึงเวลาที่จะมาลองทำ View Pager ด้วย Fragment ที่ผู้ที่หลงเข้ามาอ่านหลายๆคนรอคอยและชอบถามหากัน
บทความในซีรีย์เดียวกัน
- มารู้จักกับ Fragment กันเถอะ~
- เริ่มต้นง่ายๆกับ Fragment แบบพื้นฐาน
- ว่าด้วยเรื่องการสร้าง Fragment จาก Constructor ที่ถูกต้อง
- รู้จักกับ FragmentTransaction สำหรับการแสดง Fragment [ตอนที่ 1]
- รู้จักกับ FragmentTransaction สำหรับการแสดง Fragment [ตอนที่ 2]
- Lifecycle ของ Fragment (Fragment Lifecycle)
- วิธีการรับส่งข้อมูลของ Fragment
- มาทำ View Pager กันเถิดพี่น้อง~ [ตอนที่ 1]
- มาทำ View Pager กันเถิดพี่น้อง~ [ตอนที่ 2]
- เพิ่มลูกเล่นให้กับ View Pager ด้วย Page Transformer
ขอเกริ่นไว้สำหรับมือใหม่ที่ยังไม่รู้จักนะครับ View Pager ก็จะคล้ายกับ List View ตรงที่เอา View หลายๆ View มาเรียงกันแล้วให้เลื่อนขึ้นลงเพื่อดูแต่ละอันได้ แต่สำหรับ View Pager จะเป็นการนำ Fragment มาเรียงต่อกันในแนวนอนแล้วเลื่อนไปมาได้นั่นเอง
จะเห็นว่า View Pager มีความแตกต่างไปจาก List View ตรงที่จะมีการล็อคตำแหน่งของ Fragment แต่ละอันให้อยู่ตรงกลางจอเมื่อปล่อยนิ้ว ต่างจาก List View ที่จะไม่มีการล็อคตำแหน่ง จะเลื่อนไปที่ครึ่งๆกลางๆก็ทำได้
และเมื่อลองนึกภาพการทำงานดีๆก็จะพบว่าการทำ View Pager ด้วย Fragment แบบนี้จะค่อนข้างยืดหยุ่นเป็นอย่างมาก เพราะว่าสามารถเขียนควบคุมไว้ที่ Fragment แต่ละตัวได้เลย ว่าจะให้ Fragment ในแต่ละหน้าทำงานอะไร แต่ก็ไม่ได้หมายความว่าให้เปลี่ยน List View มาเป็น View Pager นะ เพราะว่า List View ก็ยังสะดวกในแง่ของการใช้งานที่ไม่ต้องการการทำงานที่ซับซ้อนมากนัก เหมาะสำหรับแสดงข้อมูลจำนวนมากๆ ส่วน View Pager นั้นเหมาะกับการทำงานในแต่ละหน้าที่แตกต่างกันไปหรือมีความสัมพันธ์กัน
โดย View Pager ก็จะมีหลักการง่ายๆคือ กำหนด Fragment ที่จะแสดงไว้ใน Adapter ซึ่ง Adapter จะเปรียบเสมือนตัวกลางที่จะคอยควบคุมว่า Fragment ตัวไหนอยู่หน้าไหน จากนั้นก็แสดงออกมาเป็น View Pager นั่นเอง~
ดังนั้นสิ่งที่ต้องทำก็คือ
* สร้าง View Pager ใน Layout ของ Activity
* สร้าง Fragment ที่จะใช้ใน View Pager ให้ครบ (Fragment + Layout)
* สร้าง Adapter และกำหนด Fragment ที่จะแสดงใน View Pager
* กำหนด Adapter เข้ากับ View Pager ที่สร้างไว้ใน Activity
สั้นๆง่ายๆ ส่วนความยุ่งยากก็อยู่ที่การเขียน Fragment เป็นหลักว่าจะต้องการให้ทำงานอะไรและยังไง ซึ่งก็ขึ้นอยู่กับผู้ที่หลงเข้ามาอ่านแล้วล่ะ!!
ก่อนจะเริ่ม ขออธิบายคร่าวๆก่อนนะครับว่าไฟล์ Activity, Fragment และ Layout ก็จะอิงชื่อจากบทความก่อนหน้านะครับ (แต่คำสั่งข้างในไม่เหมือนกัน)
Activity
* MainActivity.java <> activity_main.xml
Fragment
* OneFragment.java <> fragment_one.xml
* TwoFragment.java <> fragment_two.xml
* ThreeFragment.java <> fragment_three.xml
เมื่อเตรียมพร้อมแล้วก็มาเริ่มกันเลย!
สร้าง View Pager ใน Layout ของ Activity
สำหรับ View Pager นั้นจะมาพร้อมกับ Android Support v4 ดังนั้นจะไม่มี View Pager ใน API หลักนะ โดย Package ของ View Pager จะเป็น
android.support.v4.view.ViewPager
และเวลาสร้าง View Pager ใน Layout ก็จะมีลักษณะดังนี้
ยกตัวอย่างเช่น
<android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" />
เวลาประกาศในโค๊ด
ViewPager pager = (ViewPager) findViewById(R.id.pager);
ดังนั้นเจ้าของบล็อกขอสร้าง View Pager ให้กับ MainActivity เสียก่อน
<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:background="#ffffff" /> </RelativeLayout>
import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.view.ViewPager; public class MainActivity extends FragmentActivity { ViewPager pager; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pager = (ViewPager) findViewById(R.id.pager); } }
อ่ะ..หยุดไว้แค่นี้ก่อน ต่อไปมาสร้าง Fragment ที่จะแสดงบน View Pager กัน
สร้าง Fragment ที่จะมาแสดงบน View Pager
สำหรับ Fragment ขอสร้างแบบง่ายๆเลยนะ เพื่อลดความวุ่นวาย ดังนั้น Fragment ทั้ง 3 ตัว แต่ละตัวจะมีแค่ Text View อยู่ตรงกลางแล้วแสดงข้อความว่า Fragment One, Fragment Two และ Fragment Three
<?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" android:background="#e6e6e6" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment One" /> </RelativeLayout>
<?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" android:background="#e6e6e6" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment Three" /> </RelativeLayout>
<?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" android:background="#e6e6e6" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment Three" /> </RelativeLayout>
import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class OneFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_one, container, false); return rootView; } }
import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class TwoFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_two, container, false); return rootView; } }
import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class ThreeFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_three, container, false); return rootView; } }
สร้าง Adapter สำหรับ View Pager
สำหรับ Adapter จะไม่ได้ใช้ BaseAdapter หรือ SimpleAdapter แบบที่ List View ใช้นะครับ อย่าเข้าใจผิด เพราะว่า View Pager จะมี Adapter หลักๆอยู่แค่สองตัวเท่านั้น คือ FragmentStatePagerAdapter และ FragmentPagerAdapter โดยที่
* FragmentPagerAdapter (android.support.v4.app.FragmentPagerAdapter) เหมาะสำหรับ View Pager ที่มีจำนวนหน้าไม่มาก และอยากให้คงสถานะของ View อยู่ตลอดเวลา แม้แต่ Fragment จะไม่ได้ถูกแสดงก็ตาม
* FragmentStatePagerAdapter (android.support.v4.app.FragmentStatePagerAdapter) เหมาะสำหรับการใช้ View Pager แสดง Fragment จำนวนมาก เพราะจะมีการจัดการกับ Fragment เหล่านี้คล้ายๆกับ Adapter บน List View โดยจะเคลียร์การแสดงผลเมื่อ Fragment นั้นๆไม่ได้แสดงอยู่
สรุปแล้วมันต่างกันอย่างไรเนี่ย? ต่างกันประมาณนี้
สมมติว่าเจ้าของบล็อกสร้าง View Pager ที่มี Fragment อยู่ข้างใน 3 ตัว แล้วเลื่อนจากตัวแรกไปตัวสุดท้าย
FragmentPagerAdapter
FragmentStatePagerAdapter
สิ่งที่เกิดขึ้นคือ OneFragment ของ FragmentPagerAdapter จะเกิด Event เมื่อ Fragment อยู่นอกขอบเขตการแสดงผลน้อยกว่า FragmentStatePagerAdapter
ทั้งนี้ก็เพราะว่า FragmentPagerAdapter ถูกสร้างขึ้นมาสำหรับ View Pager ที่มี Fragment เป็นจำนวนไม่มากและเมื่อเคลียร์ Fragment ก็จะเคลียร์แค่ในส่วนของ View เท่านั้น ในขณะที่ FragmentStatePagerAdapter จะเหมาะกับ Fragment จำนวนมากๆ เพราะการทำงานกับ Fragment จำนวนมากจะต้องมีการเคลียร์ Fragment ที่ไม่ได้แสดงผล ดังนันจึงมีการ Detach Fragment ออกจาก View Pager
และที่สำคัญ FragmentPagerAdapter จะไม่มีการเรียก getItem ซ้ำที่ Position เก่า ส่วน FragmentStatePagerAdapter จะมีการเรียก getItem ที่ Position เก่าทุกครั้งที่กลับมาแสดง
ก็ขึ้นอยู่กับงานของผู้ที่หลงเข้ามาอ่านนะครับว่าจะใช้แบบไหน แต่ตัวอย่างของเจ้าของบล็อกไม่ได้มีอะไรมาก เป็นแค่ Fragment 3 ตัวโง่ๆที่แสดงแค่ข้อความง่ายๆเท่านั้น ดังนั้นก็ขอใช้ Adapter เป็น FragmentPagerAdapter ละกัน
สำหรับ Adapter เจ้าของบล็อกจะสร้างไฟล์ขึ้นมาใหม่ที่ชื่อว่า MyPageAdapter โดย Extend มาจาก FragmentPagerAdapter
import android.support.v4.app.FragmentPagerAdapter; public class MyPageAdapter extends FragmentPagerAdapter { public MyPageAdapter(FragmentManager fm) { super(fm); } public int getCount() { return 0; } public Fragment getItem(int position) { return null; } }
จะเห็นว่าใน Constructor ต้องมี Input Parameter เป็น FragmentManager เพื่อส่งกลับขึ้นไปผ่าน Super อีกที
getCount จะมีไว้กำหนดว่าจะให้แสดง Fragment ใน View Pager กี่ตัว
getItem มีไว้กำหนด Fragment ที่จะแสดงใน View Pager โดยมี Parameter เป็น Integer เพื่อระบุว่าเป็นของ Fragment ลำดับที่เท่าไรใน View Pager
เจ้าของบล็อกต้องการสร้าง View Pager ที่มี Fragment จำนวน 3 ตัวด้วยกัน ดังนั้นเจ้าของบล็อกก็จะกำหนดใน getCount ดังนี้
public int getCount() { return 3; }
หรือ
private final int PAGE_NUM = 3; ... public int getCount() { return PAGE_NUM; }
และในการกำหนด Fragment ที่จะแสดง ก็จะใช้วิธีง่ายๆแบบนี้
public Fragment getItem(int position) { if(position == 0) return new OneFragment(); else if(position == 1) return new TwoFragment(); else if(position == 2) return new ThreeFragment(); return null; }
จะเห็นว่าเจ้าของบล็อกใช้วิธีเทียบ Position เลยว่ามีค่าเท่าไร แล้วก็แสดง Fragment ตามที่กำหนดไว้
กำหนด Adapter ให้กับ View Pager
กลับมาที่ MainActivity ให้กำหนด Adapter ของ View Pager ให้เรียบร้อย (ลืมไปเลยล่ะสิ)
MyPageAdapter adapter = new MyPageAdapter(getSupportFragmentManager()); ViewPager pager = (ViewPager) findViewById(R.id.pager); pager.setAdapter(adapter);
import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.view.ViewPager; public class MainActivity extends FragmentActivity { ViewPager pager; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragment()); pager = (ViewPager) findViewById(R.id.pager); pager.setAdapter(adapter); } }
จะสังเกตเห็นว่า FragmentManager ที่เจ้าของบล็อกกำหนดลงไปในตอนที่ประกาศ MyPagerAdapter จะใช้เป็น getSupportFragment เพราะว่าเจ้าของบล็อกใช้ FragmentActivity ที่เป็นของ Android Support v4 นั่นเอง
จากนั้นก็ให้ลองรันทดสอบดูโดยปาดหน้าจอซ้ายขวาไปมาก็จะเห็นว่า View Pager จะแสดง Fragment ตามที่กำหนดไว้ใน Adapter
ลองควบคุม Pager ผ่าน Activity
นอกจากการเลื่อนไปมาแล้ว View Pager ยังสามารถสั่งผ่านคำสั่งได้ด้วยว่าจะให้เลื่อนไปยัง Fragment ตัวไหน ด้วยคำสั่ง
MyPageAdapter adapter = new MyPageAdapter(getSupportFragmentManager()); ViewPager pager = (ViewPager) findViewById(R.id.pager); pager.setAdapter(adapter); ... pager.setCurrentItem(position);
โดยที่ position คือ Integer ที่จะกำหนดว่าจะให้แสดง Fragment ตัวที่เท่าไร เริ่มนับจาก 0 เป็น Fragment ตัวแรกสุด
ที่หน้า Layout ของ MainActivity จะวาง Button ลงไปสองตัว แล้วเพิ่ม Margin ให้กับ View Pager เล็กน้อย จะได้เห็นขอบเขตของ View Pager (ในตัวอย่างนี้กำหนดไว้เป็นสีขาว)
<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 ให้เป็นปุ่มกดเพื่อเปลี่ยน Fragment ที่แสดงอยู่ใน View Pager นั่นเอง จากนั้นก็ประกาศ Button ใน MainActivity.java ให้เรียบร้อยซะ
package app.akexorcist.fragmentsimple; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.view.ViewPager; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; 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); Button btn_next = (Button)findViewById(R.id.btn_next); btn_next.setOnClickListener(new OnClickListener() { public void onClick(View v) { ... } }); Button btn_prev = (Button)findViewById(R.id.btn_prev); btn_prev.setOnClickListener(new OnClickListener() { public void onClick(View v) { ... } }); } }
โดยหลักการคือสมมติว่า View Pager อยู่ที่หน้าแรกสุด (Position 0) เมื่อกดปุ่ม Next ก็จะสั่งให้เลื่อนไปที่หน้าถัดไป (Position 1) เมื่อกดอีกครั้งก็จะเลื่อนไปหน้าสุดท้าย (Position 2) และเมื่อกดปุ่ม Prev ก็จะเป็นการย้อนกลับมาเรื่อยๆจนถึงหน้าแรกสุดเหมือนเดิม
ดังนั้นสิ่งที่ควรรู้คืออะไร?
ควรรู้ว่า ณ ตอนนั้น View Pager แสดงอยู่ที่หน้าเท่าไร
เพราะถ้าไม่รู้ว่ากำลังแสดงหน้าไหนอยู่ ก็ไม่รู้ว่าหน้าถัดไปควรจะเป็นหน้าอะไรนั่นเอง ซึ่ง View Pager ก็มีคำสั่งดังกล่าวไว้ให้อยู่แล้ว
int current_position= pager.getCurrentItem();
ดังนั้นการประยุกต์ใช้กับ Button ทั้ง Next และ Prev ก็จะง่ายขึ้นทันตาเห็น เพราะว่าเมื่อกดปุ่ม Next ก็จะให้บวกเข้าไปอีก 1 นั่นเอง
int current_position = pager.getCurrentItem(); int next_position = current_position + 1; pager.setCurrentItem(next_position );
หรือจะพิมพ์สั้นๆแบบนี้ก็ได้
pager.setCurrentItem(pager.getCurrentItem() + 1);
ดังนั้นสำหรับ Prev ก็เดากันได้ไม่ยากเนอะ
pager.setCurrentItem(pager.getCurrentItem() - 1);
ถ้าสมมติว่า current_position มีค่าเป็น 0 แล้วไปกดปุ่ม Prev ซ้ำมันก็กลายเป็น -1 สิ แล้วแบบนี้คำสั่งจะไม่เกิด Exception ขึ้นหรอ?
ข้อดีของคำสั่ง setCurrentItem ก็คือจะไม่มีปัญหา OutOfBoundException เพราะว่าถ้าค่าต่ำกว่า 0 ลงไป View Pager ก็จะเลือกไปที่หน้าแรกสุด (Position 0) แทน และถ้าสมมติว่า View Pager มี 3 หน้า แต่ดันไปกำหนด Position เป็น 20 ซึ่งมีค่าเกินกำหนด View Pager ก็จะเลือกไปที่หน้าสุดท้ายเท่าที่กำหนดไว้ใน Adapter แทน (Position 2)
ดังนั้น MainActivity ที่เพิ่ม Button เข้าไป 2 ตัวก็จะได้ออกมาเป็นดังนี้
package app.akexorcist.fragmentsimple; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.view.ViewPager; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; 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); Button btn_next = (Button)findViewById(R.id.btn_next); btn_next.setOnClickListener(new OnClickListener() { public void onClick(View v) { pager.setCurrentItem(pager.getCurrentItem() + 1); } }); Button btn_prev = (Button)findViewById(R.id.btn_prev); btn_prev.setOnClickListener(new OnClickListener() { public void onClick(View v) { pager.setCurrentItem(pager.getCurrentItem() - 1); } }); } }
เมื่อเสร็จแล้วก็ให้รันทดสอบดู จะสามารถกดปุ่ม Next หรือ Prev เพื่อเลื่อน Fragment ไปมาได้ หรือจะปาดนิ้วไปมาบน View Pager ก็ได้เช่นกัน
แต่ทว่าการกำหนด Fragment ใน Adapter ที่ใช้อยู่ตอนนี้จะมีข้อจำกัดอยู่อย่างหนึ่ง นั่นก็คือไม่สามารถดึง Fragment ที่แสดงอยู่บน View Pager เพื่อเรียกใช้คำสั่งบางอย่างได้
Fragment fragment = pager.getItem(0); OneFragment oneFragment = (OneFragment)fragment; oneFragment.method();
นั่นก็เพราะว่าคำสั่ง getItem ที่ใช้ดึง Fragment เจ้าของบล็อกได้ Return Fragment ด้วยการสร้าง Instance ขึ้นมาใหม่ เช่น
return new Fragment;
ดังนั้น Fragment ที่ได้ก็จะเป็นคนละอันกับที่แสดงอยู่บน View Pager ดังนั้นต่อให้เรียก Method ใดๆบน Fragment ตัวนั้นๆก็จะได้เป็น Null ทันที
ถ้า View Pager ของผู้ที่หลงเข้ามาอ่านเป็นประเภท Standalone คือ ทำงานได้ด้วยตัวเอง โดยไม่มีการส่งข้อมูลระหว่าง Activity หรือ Fragment ตัวอื่นๆ ก็อาจจะไม่มีปัญหาอะไร แต่เอาเข้าจริงเจ้าของบล็อกก็ไม่แนะนำให้เมินเฉยปัญหานี้ไป และนอกจากนี้การสร้าง Fragment ด้วย Constructor ก็ไม่ใช่วิธีที่ถูกต้องด้วย ดังนั้น View Pager จึงไม่อาจจะจบลงเพียงแค่นี้ เพราะงั้นขอให้ติดตามอ่านกันต่อที่บทความตอนที่ 2 นะคร้าบบบบบบบ