第十五章 Fragment

      在〈第十五章 Fragment〉中尚無留言

Fragment是Android 3.0以後才支援, 主要目的是讓App的執行畫面, 可以隨著手機和平板電腦的螢幕大小自動調整. 比如在手機上開啟Settings, 畫面會先顯示主項目列表, 這個主項目列表其實是一個Fragment物件, 當點選其中的項目時, 畫面會切換到另一個顯示細項的Fragment物件. 但如果是在平板上執行Settings功能, 主項目列表的Fragment和顯示細項的Fragment會同時出現. 畫面可以隨著裝置的螢幕大小. 改變顯示的方式.

Fragment為碎片的意思, 把畫面元件全都擺在Fragment裏, 再將Fragment放到Activity元件中. 一個Activity可以容納多個Fragment.

Fragment建立

首先建立二個畫面配置檔, fragment_a.xml及fragment_b.xml 如下

fragment_a.xml

android_fragmenta

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TableRow
            android:layout_weight="1">
            <ImageView
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@drawable/img1"
                android:id="@+id/imageView1" />
            <ImageView
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@drawable/img2"
                android:id="@+id/imageView2" />
        </TableRow>
        <TableRow
            android:layout_weight="1">
            <ImageView
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@drawable/img3"
                android:id="@+id/imageView3" />
            <ImageView
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@drawable/img4"
                android:id="@+id/imageView4" />
        </TableRow>
        <TableRow
            android:layout_weight="1">
            <ImageView
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@drawable/img5"
                android:id="@+id/imageView5" />
            <ImageView
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@drawable/img6"
                android:id="@+id/imageView6" />
        </TableRow>
    </TableLayout>
</LinearLayout>

fragment_b.xml

android_fragmentb

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:srcCompat="@drawable/img1"
        android:id="@+id/imageView" />
</LinearLayout>

然後再撰寫二個繼承Fragment的子類別, 把上面二個畫面放入. 放入配置畫面的時機, 是在onCreateView()的方法中實現. 每個Fragment都有自己的生命周期, 分別為

android_fragment_life

而填入配置畫面的方法, 需使用到Inflater填充器, 如下所示
inflater.inflate(R.layout.fragment_a, container, false);
填充之後所產生的是一個View物件. 在onCreateView中, 需把此View物件返回

FragmentA.class

public class FragmentA extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view=inflater.inflate(R.layout.fragment_a, container, false);
        return view;
    }
}

FragmentB.java

public class FragmentB extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view= inflater.inflate(R.layout.fragment_b, container, false);
        return view;
    }
}

主畫面的配置檔activity_main.xml 中, 先安排四顆按鈕進行畫面的切換及移除. 而於按鈕下方再置一個FragmentLayout, id設為container, 用於容納上面二個Fragment.

在activity_main.xml中, 也可以使用<fragment>標簽直接把Fragment 類別放進去
<fragment class=”com.asuscomm.mahaljsp.fragmenttest.FragmentA”
android:id=”@+id/fragmenta” />

此時MainActivity 不能使用findViewById(R.layout.fragmenta)來取得物件, 而是需要使用getFragmentManager().findFragmentById(R.layout.fragmenta)來取得物件. 再者使用此方法後, 畫面就固定不能改變了, 所以不建議這麼作

activity_main.xml 程式碼如下

android_fragment3

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.asuscomm.mahaljsp.ch14_01_fragment.MainActivity"
    android:orientation="vertical">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:onClick="btn1_click"
            android:text="畫面1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/btn1"
            android:layout_weight="1" />
        <Button
            android:onClick="btn2_click"
            android:text="畫面2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/btn2"
            android:layout_weight="1" />
    </LinearLayout>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:onClick="btn3_click"
            android:text="移除畫面1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/btn3"
            android:layout_weight="1" />
        <Button
            android:onClick="btn4_click"
            android:text="移除畫面2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/btn4"
            android:layout_weight="1" />
    </LinearLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/container">
    </FrameLayout>
</LinearLayout>

在MainActivity.java中, 直接使用setContentView(R.layout.activity_main), 程式碼如下

public class MainActivity extends AppCompatActivity {
    private FragmentManager manager;
    private int picture; //FragmentB要show的圖片
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        manager=getSupportFragmentManager();
        FragmentTransaction transaction=manager.beginTransaction();
        transaction.add(R.id.container, new FragmentA(), "FragmentA");
        transaction.commit();
    }
    public void btn1_click(View view){
        FragmentTransaction transaction=manager.beginTransaction();
        transaction.replace(R.id.container, new FragmentA(), "FragmentA");
        transaction.commit();
    }
    public void btn2_click(View view){
        FragmentTransaction transaction=manager.beginTransaction();
        transaction.replace(R.id.container, new FragmentB(), "FragmentB");
        transaction.commit();
    }
    public void btn3_click(View view){
        Fragment f=manager.findFragmentByTag("FragmentA");
        if(f!=null) {
            FragmentTransaction transaction=manager.beginTransaction();
            transaction.remove(f);
            transaction.commit();
        }
    }
    public void btn4_click(View view){
        Fragment f=manager.findFragmentByTag("FragmentB");
        if(f!=null) {
            FragmentTransaction transaction=manager.beginTransaction();
            transaction.remove(f);
            transaction.commit();
        }
    }
}

Fragment置換

在上述的MainActivity.java中, 使用Activity類別的getSupportFragmentManager()方法取得Fragment的管理器-manager.

此管理器是位於Activity中, 所以由Activity下達指令切換或刪除Fragment, 非常的方便.

進行Fragment的新增, 刪除, 替換畫面時, 都需使用FragmentTransaction交易物件進行操作. 交易物件就是由管理器manager取得的, 如下 :
manager.beginTransaction()
取得transaction 物件後, 再由transaction的方法進行操作, 如add, replace, remove

在add及replace時, 第一個參數指名要變更的是那一個容器, 第二個參數為新增的Fragment. 另外為了方便日後移除, 所以第三個參數需要再加一個Tag, 如下所述
transaction.add(R.id.container, new FragmentA(), “FragmentA”);

在remove時, 需指定要移除那一個Fragment, 取得Fragment的方法為
manager.findFragmentByTag(“FragmentA”);

當所有的Transaction都設定好動作後, 最後再加上commit()方法進行正式的操作. 注意, 一個Transaction物件, 只能操作一次commit();

Fragment互相切換

在Activity中, 只要使用manager管理器, 就可以切換FragmentA及FragmentB, 很方便

android_fragment_change1

但如果要由FragmentA切到FragmentB呢?? FragmentA並沒有管理器啊!!

android_fragment_change2

在FragmentA中, 可以使用 (MainActivity)getActivity()方法取得MainActivity物件, 然後再於Activity撰寫要切換的動作即可

所以FragmentA.Java可改寫如下

public class FragmentA extends Fragment {
    View view;
    ImageView img1, img2, img3, img4, img5, img6;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view=inflater.inflate(R.layout.fragment_a, container, false);
        processViews();
        processControllers();
        return view;
    }
    public void processViews(){
        img1=(ImageView)view.findViewById(R.id.imageView1);
        img2=(ImageView)view.findViewById(R.id.imageView2);
        img3=(ImageView)view.findViewById(R.id.imageView3);
        img4=(ImageView)view.findViewById(R.id.imageView4);
        img5=(ImageView)view.findViewById(R.id.imageView5);
        img6=(ImageView)view.findViewById(R.id.imageView6);
    }
    public void processControllers(){
        img1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((MainActivity)getActivity()).showPicture(R.drawable.img1);
            }
        });
        img2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((MainActivity)getActivity()).showPicture(R.drawable.img2);
            }
        });
        img3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((MainActivity)getActivity()).showPicture(R.drawable.img3);
            }
        });
        img4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((MainActivity)getActivity()).showPicture(R.drawable.img4);
            }
        });
        img5.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((MainActivity)getActivity()).showPicture(R.drawable.img5);
            }
        });
        img6.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((MainActivity)getActivity()).showPicture(R.drawable.img6);
            }
        });
    }
}

FragmentB.java改寫如下

public class FragmentB extends Fragment {
    private ImageView img;
    private View view;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view= inflater.inflate(R.layout.fragment_b, container, false);
        processViews();
        processControllers();
        return view;
    }
    public void processViews(){
        img=(ImageView)view.findViewById(R.id.imageView);
    }
    public void processControllers(){
        img.setImageResource(((MainActivity)getActivity()).getPicture());
    }
}

然後在MainActivity.java最後加上切換Fragment的方法

public void showPicture(int p){
    picture=p;
    FragmentTransaction transaction=manager.beginTransaction();
    transaction.replace(R.id.container, new FragmentB(), "FragmentB");
    transaction.commit();
}
public int getPicture(){return picture;}

下載完整的程式碼

ListFragment

本範例說明ListFragment的使用方式, 同時加入可同時適用於直式及橫式裝置的程式碼.

android_fragment_portrait

android_fragment_landscape

首先將要測試的選項項目置於arrays.xml中, 如下程式碼


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="movies">
        <item>ID4星際重生</item>
        <item>X戰警-天啟</item>
        <item>加州大地震</item>
        <item>阿凡達</item>
        <item>魔鬼終結者-創世契機</item>
        <item>蟻人</item>
        <item>地心引力</item>
        <item>美國隊長3.內戰英雄</item>
        <item>奇幻森林</item>
        <item>星球大戰7.原力覺醒</item>
        <item>海底總動員2.多莉去哪兒</item>
        <item>功夫熊貓3</item>
        <item>小小兵</item>
        <item>侏儸紀公園</item>
    </string-array>
    <string-array name="details">
        <item>20年後的今天, 終於出了第二集了\n實在是好看啊,但一定要看3D的喔</item>
        <item>一堆五四三的, 亂飛一通\n蠻花眼力的</item>
        <item>這部的特效作的好啊\n一定要看3D的</item>
        <item>這就真的經典了, 真的是創世之舉</item>
        <item>啟蒙我的電影\n讓我邁入Linux的世界</item>
        <item>違反物理定律的東西\n看完後內心蠻空洞的</item>
        <item>特效作的不錯\n有點科學常識</item>
        <item>也是讓人蠻眼花的</item>
        <item>效果蠻好的, 還是3D</item>
        <item>嗯, 就是星際大戰</item>
        <item>看看這動畫, 下足心力的</item>
        <item>太好笑了, 很好看</item>
        <item>感覺這片子就不太用心了</item>
        <item>外國人真的太力害了\n這到底是怎麼拍出來的啊</item>
    </string-array>
</resources>

後面我們將會自訂二個類別, 分別為ListArea及DetailArea. ListArea繼承ListFragment, 用來顯示所有movies list. 而DetailArea繼承Fragment, 是用來顯示詳細的電影介紹.

MainActivity的畫面配置檔需要使用到二個, 一個是activity_main.xml用來顯示直立時的畫面設定, 另一個是activity_main.xml(land)用來顯示橫式的畫面

在activity_main.xml中設定一個FrameLayout, id為container_portrait, 此容器可動態變更顯示ListArea或顯是DetailArea

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.asuscomm.mahaljsp.ch14_03_listfragment.MainActivity"
    android:orientation="vertical">
    <FrameLayout
        android:background="#ffff99"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/container_portrait">
    </FrameLayout>
</LinearLayout>

而activity_main.xml(land)則設定二個FrameLayout, 左邊是container_landscape_left, 右邊為container_landscape_right

<?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:orientation="horizontal">
    <FrameLayout
        android:background="#ffff99"
        android:layout_width="200dp"
        android:layout_height="match_parent"
        android:id="@+id/container_landscape_left">
    </FrameLayout>
    <FrameLayout
        android:background="#00ffff"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/container_landscape_right">
    </FrameLayout>
</LinearLayout>

完成上述設定後, 即可執行, 可分別看到直式及橫式的執行結果如下

android_listfragment_1

android_listfragment_2

顯示在ListArea及DetailArea裏面的資料, 全都要設定在MainActivity中, 所以在MainActivity就要新增二個static 字串變數, 以及一個static int position用來指定目前是點選那一個選項

另外請特別注意一件事, 當畫面旋轉時, MainActivity會作如下的事件
1. 先執行onSaveInstanceState(Bundle outState)這個方法, 然後將outState進行儲存
2. 關閉程式
3. 重新執行onCreate()並取出outState物件, 然後由savedInstanceState接收

所以我們要判定onCreate()是第一次啟動被執行的, 還是因為旋轉而被執行的, 就要判定savedInstanceState是否為null值. 如果是null, 則表示是第一次被啟動. 如果不是null, 則表示是由旋轉而觸發的

底下藍色的部份為這次新增的程式碼.  若是第一次啟動onCreate(), 則由資源檔中取得字串陣列. 而在onSaveInstanceState()中, 把position儲存起來, 保留選項被點選的位置.

public class MainActivity extends AppCompatActivity {
    static String []movies, details;
    static int position;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(savedInstanceState!=null){
            position=savedInstanceState.getInt("position");
        }
        else{
            movies=getResources().getStringArray(R.array.movies);
            details=getResources().getStringArray(R.array.details);
        }
    }
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putInt("position", position);
        super.onSaveInstanceState(outState);
    }
}

神秘的ListArea出現了, 就~~~設定一下ArrayAdapter就好了

public class ListArea extends ListFragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        setListAdapter(ArrayAdapter.createFromResource(getActivity(), R.array.movies, android.R.layout.simple_list_item_1));
        return super.onCreateView(inflater, container, savedInstanceState);
    }
}

上面的ListArea出現後, MainActivity就有的改了. 要新增一個FragmentManager manager. 這個管理器物件是由Activity產生的, 而每次旋轉後就會重新產生新的Activity, 因此這個物件也要跟著再重新再產生一次.
要新增的部份如下面藍色的部份

public class MainActivity extends AppCompatActivity {
    static String []movies, details;
    static int position;
    FragmentManager manager;//每次旋轉都會產生新的Activity, 所以要重設manager, 因此不能用static
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(savedInstanceState!=null){
            position=savedInstanceState.getInt("position");
        }
        else{
            movies=getResources().getStringArray(R.array.movies);
            details=getResources().getStringArray(R.array.details);
        }
        manager=getFragmentManager();
        FragmentTransaction transaction=manager.beginTransaction();
        if(getResources().getConfiguration().orientation== Configuration.ORIENTATION_PORTRAIT){
            transaction.replace(R.id.container_portrait, new ListArea(), "ListArea");
        }
        else{
            transaction.replace(R.id.container_landscape_left, new ListArea(), null);
        }
        transaction.commit();
    }
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putInt("position", position);
        super.onSaveInstanceState(outState);
    }
}

那詳細的電影介紹呢, 要如何顯示? 當然就得再新增一個畫面配置檔, 然後產生DetailArea的class來顯示.

下面的程式碼, 在橫式時, 就會顯示出效果了(直立裝置還需後續的說明)

畫面配置檔 detail_area.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/txtMovie" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/txtDetail" />
</LinearLayout>

DetailArea.java

public class DetailArea extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view=inflater.inflate(R.layout.detail_area, container, false);
        int position=((MainActivity)getActivity()).position;
        TextView txtMovie=(TextView)view.findViewById(R.id.txtMovie);
        TextView txtDetail=((TextView)view.findViewById(R.id.txtDetail));
        txtMovie.setText(((MainActivity)getActivity()).movies[position]);
        txtDetail.setText(((MainActivity)getActivity()).details[position]);
        return view;
    }
}

當list 選項點選之後, 要show出電影詳細的介紹, 該如何達成此功能呢? 很簡單, 在ListArea class中加入 onListItemClick()即可

@Override
public void onListItemClick(ListView list, View view, int position, long id) {
    ((MainActivity)getActivity()).showDetail(position);
}

此時showDetail(position)會出現錯誤訊息, 因為MainActivity還沒有showDetail()的方法. showDetail()需判斷是否為直立, 如果為直立, 就用新的DetailArea取代container_portrait容器. 若為橫式, 則使用新的DetailArea取代container_landscape_right容器

另在MainActivity再加入onBackPressed(), 用來控制是否要退出此程式

public void showDetail(int p){
    position=p;
    FragmentTransaction transaction=manager.beginTransaction();
    if(getResources().getConfiguration().orientation==Configuration.ORIENTATION_PORTRAIT){
        transaction.replace(R.id.container_portrait, new DetailArea(), "DetailArea");
    }
    else{
        transaction.replace(R.id.container_landscape_right, new DetailArea(), null);
    }
    transaction.commit();
}

@Override
public void onBackPressed() {
    if(getResources().getConfiguration().orientation==Configuration.ORIENTATION_PORTRAIT
            && manager.findFragmentByTag("DetailArea")!=null){
        FragmentTransaction transaction=manager.beginTransaction();
        transaction.replace(R.id.container_portrait, new ListArea(), "ListArea");
        transaction.commit();
    }
    else {
        super.onBackPressed();
    }
}

完整程式碼下載

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *