อยู่ในระหว่างการปรับปรุง
เนื่องจากบทความตอนที่ 1 ไม่อาจจะลากยาวไปมากกว่านี้ได้แล้ว จึงขอแบ่งครึ่งออกมาต่อเป็นตอนที่ 2 ที่บทความนี้แทนนะครับ
บทความในซีรีย์เดียวกัน
- มารู้จักกับ 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
จากตอนที่ 1 เจ้าของบล็อกได้จบท้ายบทไว้ว่าการทำ View Pager จากตอนแรกจะมีปัญหาอยู่สองอย่างคือ เมื่อเรียกคำสั่ง getItem ทุกๆครั้ง Fragment ที่ได้จะเป็นคนละตัวกับของเดิมโดยสิ้นเชิง และอีกปัญหาคือการใช้ Constructor ในการสร้าง Fragment ใน Adapter นั้นไม่ใช่วิธีที่ถูกต้อง
มาดูกันว่าทำไม เพราะอะไร แล้วจะแก้ปัญหาเหล่านี้ยังไงดี
ทำไมการใช้ Constructor ในการสร้าง Fragment สำหรับ View Pager ถึงไม่ถูกต้อง?
จากที่บอกในตอนแรกไปว่า View Pager นั้นทำงานคล้ายๆกับ List View นั้นก็คือ Fragment ที่ไม่แสดงผลจะมีการ Destroy ทิ้ง แล้วจึงสร้างขึ้นมาใหม่อีกครั้งเมื่อต้องการใช้งาน แต่ทว่าตอนที่ Fragment ถูกเรียกขึ้นมาแสดงใหม่อีกครั้งนั้น เมธอด getItem กลับไม่ได้ถูกเรียกขึ้นมาใหม่เหมือนกับ getView ของ List View
public class MyPagerAdapter extends FragmentPagerAdapter { ... public Fragment getItem(int position) { if(position == 0) { Log.i("Check", "Get Item 0"); return new OneFragment("Android,Development"); } ... } }
public class OneFragment extends Fragment { String str; public OneFragment(String str) { this.str = str; } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.i("Check", "OnCreateView"); String[] text = str.split(","); Log.i("Check", text[0]); ... } public void onDestroyView() { Log.i("Check", "OnDestroyView"); } }
* ใน Constructor ของ OneFragment (Fragment ตัวแรกสุดใน View Pager) มีการสร้าง Parameter เป็น String เพื่อเอาไว้ใช้งานใน onCreateView (สมมติว่าเอาไป Split String เพื่อนำไปใช้งานอีกทีหนึ่ง)
* ที่ Adapter ให้ใช้คำสั่ง Log เพื่อแสดงข้อความอะไรก็ได้ใน getView ของ Position 0 (Fragment ตัวแรก) แล้วทำการสร้าง Fragment ด้วย Constructor โดยมีการกำหนด String เข้าไปด้วย
* ที่คลาส Fragment ของ Position 0 ให้ใช้คำสั่ง Log เพื่อแสดงข้อความอะไรก็ได้ใน onCreateView และ onDestroyView
* รันแอพเพื่อทดสอบ เมื่อแอพเปิดขึ้นมาจะพบว่า getItem ทำงานทันทีเพื่อแสดง Fragment ใน View Pager
* ทดสอบด้วยการเลื่อนไปจนถึง Fragment อันสุดท้าย (Position 2) จะพบว่า Fragment แรกสุดถูก Destroy ทิ้ง (สังเกตจาก Log ที่ให้ใส่ไว้)
* เลื่อนกลับมาที่ Fragment อันแรกสุด (Position 0) จะพบว่า Fragment แรกสุดถูกสร้างขึ้นมาใหม่ (สังเกตจาก Log ที่ให้ใส่ไว้)
* ถ้าสังเกตดีๆจะเห็นว่าตอนที่ Fragment ตัวแรกถูกแสดงขึ้นมาใหม่ getItem นั้นกลับไม่ทำงาน
แต่คำว่า “Android” ก็ยังแสดงใน Log ได้ปกติไม่ใช่หรือ?
ทั้งนี้ก็เพราะว่า Memory ยังคงเก็บค่าของ OneFragment ไว้อยู่นั่นเอง และตอนที่เลื่อน View Pager เป็นหน้าอื่นๆก็ไม่ได้ทำอะไรจึงทำให้ไม่มีการใช้ Memory ซักเท่าไรนัก
แต่ในการเขียนแอพจริงๆ Fragment บางหน้าอาจจะมีการใช้ Memory เยอะ
ยกตัวอย่างเช่น Fragment หน้าสุดท้ายมีการโหลดภาพขนาดใหญ่มาแสดง จึงทำให้มีการใช้ Memory เยอะ ดังนั้น Memory ในส่วนที่จำข้อมูลของ OneFragment ก็จะถูกเคลียร์ทิ้งเพื่อนำไปใช้กับการแสดงภาพใน Fragment หน้าที่แสดงอยู่
ผลก็คือ เมื่อกลับมาที่ OneFragment ในหน้าแรกอีกครั้ง จะทำให้ str มีค่าเป็น Null เพราะ Memory ถูกเคลียร์ทิ้งไปแล้ว และ getItem ก็ไม่ถูกเรียกเพื่อกำหนดค่าใหม่
นี่จึงเป็นปัญหาที่ทำให้การใช้ Constructor มาสร้าง Fragment นั้นไม่เหมาะสม
แล้วควรแก้ปัญหานี้อย่างไร?
ควรใช้ Static เข้ามาช่วยเพื่อสร้าง Fragment ด้วย Constructor อีกที
public class OneFragment extends Fragment { public static OneFragment newInstance() { OneFragment fragment = new OneFragment(); return fragment; } public OneFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... } ... }
และเมื่อต้องการส่ง String เข้ามาด้วย ก็จะใช้ Argument เข้ามาช่วยแทน โดยยัดข้อมูลเก็บไว้ใน Bundle ไม่แนะนำให้โยนขึ้น Global เพื่อส่งข้ามจาก Static ไปยัง Non-Static นะ
public class OneFragment extends Fragment { private static final String KEY_STRING = "key_string"; public static OneFragment newInstance(String str) { OneFragment fragment = new OneFragment(); Bundle bundle = new Bundle(); bundle.putString(KEY_STRING, str); fragment.setArgument(bundle); return fragment; } public OneFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { String str = getArguments().getString(KEY_STRING); String[] text = str.split(","); Log.i("Check", text[0]); ... } ... }
สำหรับ Argument ก็จะคล้ายๆกับ Intent ที่คุ้นเคยกันดีนั่นเอง
จากตัวอย่างข้างต้นนี้ การใช้ Static จะช่วยให้ค่าต่างๆที่ Activity ส่งมายัง Fragment นั้นไม่กลายเป็น Null เมื่อถูกเคลียร์ Memory ทิ้ง
กลับมาดูที่ Adapter กันต่อ เมื่อเปลี่ยนจากการเรียก Constructor โดยตรงมาเป็น newInstance แทนก็จะต้องเปลี่ยนคำสั่งใน getItem ดังนี้
public class MyPagerAdapter extends FragmentPagerAdapter { ... public Fragment getItem(int position) { if(position == 0) { return OneFragment.newInstance("Android,Development"); } ... } }
ดังนั้น Fragment ทุกตัวที่จะเรียกใช้ใน View Pager จะต้องใช้วิธี newInstance ทั้งหมด
ขอสรุปโค๊ดทั้งหมดในตอนนี้ต่อจากบทความตอนที่แล้วอีกรอบหนึ่ง
* MainActivity.java < > activity_main.xml
* MyPagerAdapter.java
* OneFragment.java < > fragment_one.xml
* TwoFragment.java < > fragment.two.xml
* ThreeFragment.java < > fragment.three.xml
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); } }); } }
<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>
import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; public class MyPageAdapter extends FragmentPagerAdapter { private final int NUM_ITEMS = 3; public MyPageAdapter(FragmentManager fm) { super(fm); } 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; } }
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 static OneFragment newInstance() { OneFragment fragment = new OneFragment(); return fragment; } public OneFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_one, container, false); return rootView; } }
<?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:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment One" /> </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 TwoFragment extends Fragment { public static TwoFragment newInstance() { TwoFragment fragment = new TwoFragment(); return fragment; } public TwoFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_two, container, false); return rootView; } }
<?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:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment Two" /> </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 ThreeFragment extends Fragment { public static ThreeFragment newInstance() { ThreeFragment fragment = new ThreeFragment(); return fragment; } public ThreeFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_three, container, false); return rootView; } }
<?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:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment Three" /> </RelativeLayout>
เอาล่ะ! แก้ข้อบกพร่องของการใช้ Constructor โดยตรงได้แล้ว ปัญหาต่อมาก็คือ
Activity จะเรียกใช้งาน Fragment ได้อย่างไร?
ต้องบอกเลยว่าให้ลืม getItem ไปซะ เพราะว่าเจ้าของบล็อกจะไม่เรียกผ่านเมธอดนั้นเด็ดขาด เนื่องจากมันเป็นการสร้าง Instance ตัวใหม่ขึ้นมา ดังนั้นเจ้าของบล็อกจะใช้ฟังก์ชันนี้ในการเรียก Fragment ที่ต้องการ
public Fragment getActiveFragment(ViewPager container, int position) { String name = "android:switcher:" + container.getId() + ":" + position; return getSupportFragmentManager().findFragmentByTag(name); }
ฟังก์ชันนี้มีไว้สำหรับค้นหา Fragment ที่อยู่ใน View Pager โดยอิงจากชื่อ Tag
ก่อนอื่นเจ้าของบล็อกขอเตรียมการเล็กน้อยก่อน ขอเพิ่มคำสั่งสำหรับ Text View ที่อยู่ใน OneFragment ดังนี้
public class OneFragment extends Fragment { TextView textView1; ... public View onCreateView(LayoutInflater inflate, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_one, container, false); textView1 = (TextView)rootView.findViewById(R.id.textView1); return rootView; } public String getMyText() { return textView1.getText().toString(); } }
เจ้าของบล็อกได้สร้างเมธอดให้กับ OneFragment ที่มีชื่อว่า getMyText โดยจะให้ Return ข้อความที่แสดงใน @+id/textView1 ของ fragment_one.xml (ข้อความว่า “Fragment One” นั่นเอง)
จากนั้นกลับมาที่ activity_main.xml ต่อ ที่ตรงนี้ขอเพิ่ม Button ธรรมดาเข้าไป 1 ตัวโดยตั้งชื่อ ID ว่า @+id/btn_request
<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> <Button android:id="@+id/btn_request" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:text="Request" /> </RelativeLayout>
และประกาศ Button และ OnClickListener เพิ่มเข้าไปใน MainActivity.java
public class MainActivity extends FragmentActivity { ... ViewPager pager; protected void onCreate(Bundle savedInstanceState) { ... pager = (ViewPager) findViewById(R.id.pager); ... Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { } }); } public Fragment getActiveFragment(ViewPager container, int position) { String name = "android:switcher:" + container.getId() + ":" + position; return getSupportFragmentManager().findFragmentByTag(name); } }
อ่ะ เตรียมเสร็จละ! ต่อจากเดิมที่พูดค้างไว้นะครับ เจ้าของบล็อกต้องการดึง OneFragment ที่อยู่ใน Position 0 ของ View Pager ก็จะต้องใช้คำสั่งดังนี้
Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { Fragment fragment = getActiveFragment(pager, 0); OneFragment oneFragment = (OneFragment)fragment; String message = oneFragment.getMyText(); Log.i("Check", message); } });
คงเข้าใจกันไม่ยากเนอะ? ก็คือ View Pager เจ้าของบล็อกตั้งชื่อไว้ว่า pager และ OneFragment ก็อยู่หน้าแรกสุดของ View Pager (Position 0) และเมื่อดึง Fragment ที่ต้องการออกมาได้แล้วก็แปลงเป็น OneFragment แล้วก็เรียกเมธอด getMyText เพื่อดึง String ของ @+id/textView1 ที่อยู่ใน fragment_one.xml มาแสดงใน LogCat
ให้ลองรันทดสอบแล้วกดปุ่ม Request ดู จะพบว่ามีข้อความที่แสดงอยู่ใน OneFragment ไปโผล่ใน LogCat แล้ว นั่นก็หมายความว่าผู้ที่หลงเข้ามาอ่านสามารถเรียก Fragment ผ่าน Activity ได้แล้วนั่นเอง
ทีนี้ให้ลองเพิ่มคำสั่งที่ ThreeFragment.java ให้เหมือนกับใน OneFragment.java (เพิ่ม Text View และ getMyText)
public class ThreeFragment extends Fragment { TextView textView1; ... public View onCreateView(LayoutInflater inflate, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_three, container, false); textView1 = (TextView)rootView.findViewById(R.id.textView1); return rootView; } public String getMyText() { return textView1.getText().toString(); } }
ส่วน MainActivity.java ให้แก้ไขจาก OneFragment เป็น ThreeFragment แทน โดยต้องเปลี่ยนจาก 0 เป็น 2 และแปลง Fragment เป็น ThreeFragment
Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { Fragment fragment = getActiveFragment(pager, 2); ThreeFragment threeFragment = (ThreeFragment)fragment; String message = threeFragment.getMyText(); Log.i("Check", message); } });
ให้รันทดสอบอีกครั้ง เมื่อแอพเปิดขึ้นมาก็ให้ลองกดปุ่ม Request ทันที
ถูกต้องแล้วครับ NullPointerException ครับ เพราะว่าหน้า ThreeFragment ยังไม่ถูกสร้างขึ้นนั่นเอง จะมีแค่ OneFragment กับ TwoFragment แค่สองอันที่ถูกสร้างขึ้น ณ ตอนนี้ ซึ่งต้องเลื่อนไปหน้า TwoFragment ก่อน View Pager จึงจะสร้าง ThreeFragment ขึ้นมา
ดังนั้นการจะดึง ThreeFragment มาใช้จะมีเงื่อนไขว่า Fragment นั้นๆต้องถูกสร้างขึ้นมาก่อน ไม่เช่นนั้นจะเป็น Null ไปในทันที ดังนั้นทางที่ดีเมื่อใช้คำสั่ง getActiveFragment ก็ควรจะเช็ค Null ด้วยทุกครั้ง
Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { Fragment fragment = getActiveFragment(pager, 2); ThreeFragment threeFragment = (ThreeFragment)fragment; if(threeFragment != null) { String message = threeFragment.getMyText(); Log.i("Check", message); } } });
เพียงเท่านี้ก็สามารถเรียก Fragment ผ่าน Activity ได้แล้ว~♪
และสำหรับการเรียก Activity ผ่าน Fragment ก็ทำเหมือนเดิมได้เลย
MainActivity mainActivity = (MainActivity)getActivity();
ถึงตอนนี้แล้ว ผู้ที่หลงเข้ามาอ่านน่าจะเริ่มจับเคล็ดการทำงานของ Fragment บน View Pager กันได้บ้างแล้วเนอะ? เพราะถ้ายังจับเคล็ดไม่ได้ก็จะไล่ให้กลับไปอ่านใหม่ตั้งแต่ต้น ฮ่าๆ
เหมือนเดิมนะครับ บทความนี้ไม่มีแจกตัวอย่าง เพราะอยากจะให้ผู้ที่หลงเข้ามาอ่านได้ลองทำตามเพื่อทำความเข้าใจด้วยตนเองมากกว่าการดาวน์โหลดโค๊ดไปดู
View Pager เบื้องต้นสำหรับ Fragment เสร็จแล้ว~