如何使用Fragment建立TabActivity之一

如何使用Fragment建立TabActivity之一

下面的方法又被google丟掉了, 如果想要換成新版的TabHost請參考
如何使用TabLayout
以及進階應用
如何使用Toolbar+DrawerLayout+Tab+SwipRefresh


在之前如何使用Tab(分頁)的教學,
會產生很多問題(例如Back問題、切換activity的時候狀態儲存問題…等等),
因此Android開發了一個新的類別Fragment,
利用這個類別來開發TabActivity, 可以解決掉上述那些問題,
Fragment是3.0以後才出現的類別庫, 因此要下載官方的support library.

如果要下載官方的Support library,

你要先打開eclipse->window->Android SDK Manager-> 選擇Extra的Android Support下載

然後在你的android sdk資料夾之下,看到多出一個extras的資料夾, 以我的資料夾為例子

\android-sdk-windows\extras\android\support\v4\android-support-v4.jar

, 這個就是我們所需要的support library.

當我們建立一個專案的時候, 只要在專案下面在建立一個lib, 然後把android-support-v4.jar加進去,
接著點選專案右鍵, 選擇properties->Libraries->Add Library 將lib資料夾內的android-support-v4.jar加進去。

記得要到Order and Export那邊將android-support-v4.jar打勾。

如此一來就可以使用這個library的所有類別了。
官方說明
我們可以看到這個類別庫大致上有以下這幾類

Fragment
FragmentManager
FragmentTransaction
ListFragment
DialogFragment
LoaderManager
Loader
AsyncTaskLoader
CursorLoader

另外這邊有一個重點就是, 為了讓之前Android版本能夠跑我們的程式,
可以在AndroidManifest.xml上面進行設定。

<uses-sdk android:minSdkVersion="4" />

這樣一來, 無論手機上的android版本多舊, 我們仍然可以跑我們要的程式。
接下來進入我們的重點了, 如何使用TabActivity,
其實在我們下載android-support-v4.jar裡面, 就已經存在很多範例了,
但是為了簡化他的範例, 因此我用官網所提供的範例進行簡化。

首先新增一個fragment_tabs.xml

<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="0"/>

        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>

        <HorizontalScrollView 
            android:layout_height="wrap_content"
            android:layout_width="fill_parent" 
            android:scrollbars="none"
            android:id="@+id/scroller"> 

            <TabWidget
                android:id="@android:id/tabs"
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0"/>
         </HorizontalScrollView>

    </LinearLayout>
</TabHost>

這邊我們建立一個TabHost的元件, 然後我想讓TabWidget能夠呈現在下方,
因此用LinearLayout包起來,
讓上方為一個FrameLayout(橙色部分), 下方才是我們的TabWidget(藍色部分)。

首先要建立一個FragmentActivity的子類別,

public class FragmentTabs extends FragmentActivity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

基本上跟一般的Activity沒什麼不一樣, 也是先建立一個onCreate的方法。

接著我們就把TabWidget元件從xml取出, 並且初始化。

public class FragmentTabs extends FragmentActivity{
    private TabHost mTabHost;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_tabs);
        mTabHost = (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setup();
    }
}

在這邊要注意一下, 在之前我們的類別是繼承TabActivity,
因此我們常常只需要getTabHost(); 就可以取得TabHost的物件了,
原因出在我們的TabActivity裡面就對TabHost元件初始化了,
但是現在是從xml取出物件, 因此要多加上setup()這個方法,
才能夠進行add的動作。

官網說法

Call setup() before adding tabs if loading TabHost using findViewById(). However: You do not need to call setup() after getTabHost() in TabActivity. 

可以參考看看官方文件

接著我們必須宣告一個TabManager來處理當切換分頁的時候, 必須進行的一些動作。
類別長這樣:

public class TabManager implements TabHost.OnTabChangeListener {
    private final FragmentActivity mActivity;
    private final TabHost mTabHost;
    private final int mContainerId;
    private final HashMap<String, TabInfo> mTabs = new HashMap<String, TabInfo>();
    TabInfo mLastTab;

    static final class TabInfo {
        private final String tag;
        private final Class<?> clss;
        private final Bundle args;
        private Fragment fragment;

        TabInfo(String _tag, Class<?> _class, Bundle _args) {
            tag = _tag;
            clss = _class;
            args = _args;
        }
    }

    static class DummyTabFactory implements TabHost.TabContentFactory {
        private final Context mContext;

        public DummyTabFactory(Context context) {
            mContext = context;
        }

        @Override
        public View createTabContent(String tag) {
            View v = new View(mContext);
            v.setMinimumWidth(0);
            v.setMinimumHeight(0);
            return v;
        }
    }

    public TabManager(FragmentActivity activity, TabHost tabHost, int containerId) {
        mActivity = activity;
        mTabHost = tabHost;
        mContainerId = containerId;
        mTabHost.setOnTabChangedListener(this);
    }

    public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
        tabSpec.setContent(new DummyTabFactory(mActivity));
        String tag = tabSpec.getTag();

        TabInfo info = new TabInfo(tag, clss, args);

        info.fragment = mActivity.getSupportFragmentManager().findFragmentByTag(tag);
        if (info.fragment != null && !info.fragment.isDetached()) {
            FragmentTransaction ft =  mActivity.getSupportFragmentManager().beginTransaction();
            ft.detach(info.fragment);
            ft.commit();
        }

        mTabs.put(tag, info);
        mTabHost.addTab(tabSpec);
    }

    @Override
    public void onTabChanged(String tabId) {
     TabInfo newTab = mTabs.get(tabId);

        if (mLastTab != newTab) {
            FragmentTransaction ft = mActivity.getSupportFragmentManager().beginTransaction();
            if (mLastTab != null) {
                if (mLastTab.fragment != null) {
                    ft.detach(mLastTab.fragment);
                }
            }

            if (newTab != null) {
                newTab.fragment = Fragment.instantiate(mActivity,
                        newTab.clss.getName(), newTab.args);
                ft.add(mContainerId, newTab.fragment, newTab.tag);
                if (newTab.fragment == null) {
                    ft.detach(mLastTab.fragment);
                } else {
                    mActivity.getSupportFragmentManager().popBackStack();
                    ft.replace(mContainerId, newTab.fragment);
                    ft.attach(newTab.fragment);
                }
            }

            mLastTab = newTab;
            ft.commit();
            mActivity.getSupportFragmentManager().executePendingTransactions();
        }

    }
}

超複雜嗎? 你也可以不用管這個類別直接拿過用即可, 新建一個TabManager.java, 然後把上面內容複製貼上就可以直接使用了!

那麼以下內容你可以直接跳過

----------------------------跳過-------------------------------------------

基本上是照著官網上所寫的來進行,

你會發現在這個類別當中, 存在兩個靜態類別, 分別是TabInfo以及DummyTabFactory。

static final class TabInfo {
    private final String tag;
    private final Class<?> clss;
    private final Bundle args;
    private Fragment fragment;

    TabInfo(String _tag, Class<?> _class, Bundle _args) {
        tag = _tag;
        clss = _class;
        args = _args;
    }
}

TabInfo就是每一個分頁當中存在的資訊, 例如該分頁的名稱、傳到哪一個類別以及傳過去的時候, 所帶的Bundle。

static class DummyTabFactory implements TabHost.TabContentFactory {
    private final Context mContext;
    public DummyTabFactory(Context context) {
        mContext = context;
    }

    @Override
    public View createTabContent(String tag) {
        View v = new View(mContext);
        v.setMinimumWidth(0);
        v.setMinimumHeight(0);
        return v;
    }
}

DummyTabFactory目的是用來創造一個分頁所形成的View, 當你按下任一分頁, 它就會根據對應的View來進行切換。

public TabManager(FragmentActivity activity, TabHost tabHost, int containerId) {
    mActivity = activity;
    mTabHost = tabHost;
    mContainerId = containerId;
    mTabHost.setOnTabChangedListener(this);
}

一開始從建構子傳入的就是該FragmentActivity, 再來就是TabHost物件, 最後是一個容器的id,
這個容器就是用來裝我們切換分頁的view, 還記得fragment_tabs.xml當中, 我們用了兩個FrameLayout嗎? 這個就是放在上面的那個, 你可以對照一下後面我們會傳入的id, 就可以清楚的明白這個layout的功用。

public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
    tabSpec.setContent(new DummyTabFactory(mActivity));
    String tag = tabSpec.getTag();

    TabInfo info = new TabInfo(tag, clss, args);

    info.fragment = mActivity.getSupportFragmentManager().findFragmentByTag(tag);
    if (info.fragment != null && !info.fragment.isDetached()) {
        FragmentTransaction ft = mActivity.getSupportFragmentManager().beginTransaction();
        ft.detach(info.fragment);
        ft.commit();
    }

    mTabs.put(tag, info);
    mTabHost.addTab(tabSpec);
}

這邊就是要新增一個分頁, 就必須傳入TabHost.TabSpec物件, 以及你要轉跳的java檔案, 到時候會傳到那個Fragment, 最後是如果你有需要將此Activity的一些內容(如String, int..等等)的資料傳到指定的分頁, 就可以在最後一個參數利用Bundle物件夾帶, 如果沒有, 則可以指定null。

那麼一開始他就將TabSpec的物件帶入new出來的DummyTabFactory, 方便這個分頁的內容呈現一個View。
接著又把傳入的參數丟進去TabInfo物件, 完成初始化。
當我們要進行切換fragment的時候, 就會利用FragmentTransaction的物件來進行調度,而當commit出去的時候, 就可以進行變更。
當我們進行detach動作的時候, 就表示指定該fragment會出現在某一個View當中。
當我們進行detach動作的時候, 就表示指定該fragment會從該View當中分離。
當我們進行replace動作的時候, 才是表示將某一個fragment轉換成另外一個fragment。

官網寫法

@Override
public void onTabChanged(String tabId) {
    TabInfo newTab = mTabs.get(tabId);
    if (mLastTab != newTab) {
        FragmentTransaction ft = mActivity.getSupportFragmentManager().beginTransaction();
        if (mLastTab != null) {
           if (mLastTab.fragment != null) {
              ft.detach(mLastTab.fragment);
           }
        }
        if (newTab != null) {
            if (newTab.fragment == null) {
                newTab.fragment = Fragment.instantiate(mActivity,
                    newTab.clss.getName(), newTab.args);
                ft.add(mContainerId, newTab.fragment, newTab.tag);
            } else {
                ft.attach(newTab.fragment);
            }
        }

        mLastTab = newTab;
        ft.commit();
        mActivity.getSupportFragmentManager().executePendingTransactions();
    }
}

一開始先判斷按下的分頁是不是前一個分頁, 如果不是, 則進行處理,
那一開始我們就會把前一個分頁內容做檢查, 看是不是空的,
如果不是空的, 則把前一個分頁從分頁管理者的內容中移除,
再來就是檢查我們新的分頁是否有內容, 如果沒有,
我們就new一個空間讓他建立新的分頁,
然後加入到分頁管理者的儲存器當中,
如果有內容, 則直接指派該分頁是我們目前要呈現的分頁,
最後確定以後, 就把新的分頁跟舊的分頁設定為同一個分頁,
接著送交, 進行更新, 接著執行分頁轉換。

但是! 官網的做法讓我產生一些問題, 因此我做了修改,
大致上如下:

@Override
public void onTabChanged(String tabId) {
    TabInfo newTab = mTabs.get(tabId);
        if (mLastTab != newTab) {
            FragmentTransaction ft = mActivity.getSupportFragmentManager().beginTransaction();
            if (mLastTab != null) {
                if (mLastTab.fragment != null) {
                    ft.detach(mLastTab.fragment);
                }
            }

            if (newTab != null) {
                newTab.fragment = Fragment.instantiate(mActivity,
                        newTab.clss.getName(), newTab.args);
                ft.add(mContainerId, newTab.fragment, newTab.tag);
                if (newTab.fragment == null) {
                    ft.detach(mLastTab.fragment);
                } else {

                    ft.replace(mContainerId, newTab.fragment);
                    ft.attach(newTab.fragment);
                }
            }

            mLastTab = newTab;
            ft.commit();
            mActivity.getSupportFragmentManager().executePendingTransactions();
        }

}

這樣就可以成功解決我的問題。

如果這邊讓你覺得看不太懂, 沒關係, 直接Copy&Paste過去就好XD

--------------------結束-------------------------------------------

接著我們在onCreate就可以使用這個類別來操作我們的TabHost物件了。

public class FragmentTabs extends FragmentActivity{
    private TabHost mTabHost;
    private TabManager mTabManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_tabs);
        mTabHost = (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setup();

        mTabManager = new TabManager(this, mTabHost, R.id.realtabcontent);
    }
}

這樣基本TabHost的物件就初始化完成了。

讓我們利用TabManager類別的物件來操作這個TabHost吧!

public class FragmentTabs extends FragmentActivity{
    private TabHost mTabHost;
    private TabManager mTabManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_tabs);
        mTabHost = (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setup();

        mTabManager = new TabManager(this, mTabHost, R.id.realtabcontent);
        mTabHost.setCurrentTab(0);//設定一開始就跳到第一個分頁
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment1").setIndicator("Fragment1"),
            Fragment1.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment2").setIndicator("Fragment2"),
            Fragment2.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment3").setIndicator("Fragment3"),
            Fragment3.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment4").setIndicator("Fragment4"),
            Fragment4.class, null);
    }
}

我們加入了四個子類別, 分別是Fragment1~Fragment4,
這樣代表在切換分頁的時候, 會根據指定的分別跳置不同的Fragment。

在這邊我們必須再新增4個java檔案, 檔名當然就是Fragment1~Fragment4。
在eclipse點右鍵,
選擇sourece->override/overload 就可以找到onCreate&onCreateView這兩個方法了!

public class Fragment1 extends Fragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        return super.onCreateView(inflater, container, savedInstanceState);
    }
}

其他的四個類別也相同。

那麼跑看看模擬器, 就會出現這樣。

怎麼會擠在一團咧XD
沒關係, 做一下調整。

public class FragmentTabs extends FragmentActivity{
    private TabHost mTabHost;
    private TabManager mTabManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_tabs);
        mTabHost = (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setup();

        mTabManager = new TabManager(this, mTabHost, R.id.realtabcontent);
        mTabHost.setCurrentTab(0);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment1").setIndicator("Fragment1"),
            Fragment1.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment2").setIndicator("Fragment2"),
            Fragment2.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment3").setIndicator("Fragment3"),
            Fragment3.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment4").setIndicator("Fragment4"),
            Fragment4.class, null);

        DisplayMetrics dm = new DisplayMetrics();   
        getWindowManager().getDefaultDisplay().getMetrics(dm); //先取得螢幕解析度  
        int screenWidth = dm.widthPixels;   //取得螢幕的寬


        TabWidget tabWidget = mTabHost.getTabWidget();   //取得tab的物件
        int count = tabWidget.getChildCount();   //取得tab的分頁有幾個
        if (count > 3) {   
            for (int i = 0; i < count; i++) {   
                tabWidget.getChildTabViewAt(i)
                      .setMinimumWidth((screenWidth)/3);//設定每一個分頁最小的寬度   
            }   
        }
    }
}

一開始就先取得螢幕的解析度, 接著由於我們是x軸要加寬, 所以只要取得螢幕的寬度即可,
再來算出我們一共用了幾個分頁, 假設你只想要呈現3個分頁, 因此就把螢幕的寬度除以3個分頁, 這樣一來, 畫面就會把第4個分頁放在螢幕外面, 當你需要第4個分頁的時候, 只需要拉動某一個分頁, 就會出現其他被隱藏的分頁了。

你看是不是好多了?

只要往左拉, 第4個分頁就出現了。

可是總覺得少了那麼一點東西, 加個icon好了。

public class FragmentTabs extends FragmentActivity{
    private TabHost mTabHost;
    private TabManager mTabManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_tabs);
        mTabHost = (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setup();

        mTabManager = new TabManager(this, mTabHost, R.id.realtabcontent);
        mTabHost.setCurrentTab(0);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment1").setIndicator("Fragment1",
            this.getResources().getDrawable(
                android.R.drawable.ic_dialog_alert)),Fragment1.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment2").setIndicator("Fragment2",
            this.getResources().getDrawable(
                android.R.drawable.ic_lock_lock)),Fragment2.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment3").setIndicator("Fragment3",
            this.getResources().getDrawable(
                android.R.drawable.ic_input_add)),Fragment3.class, null);
        mTabManager.addTab(
            mTabHost.newTabSpec("Fragment4").setIndicator("Fragment4",
            this.getResources().getDrawable(
                android.R.drawable.ic_delete)),Fragment4.class, null);
        DisplayMetrics dm = new DisplayMetrics();   
        getWindowManager().getDefaultDisplay().getMetrics(dm); //先取得螢幕解析度  
        int screenWidth = dm.widthPixels;   //取得螢幕的寬


        TabWidget tabWidget = mTabHost.getTabWidget();   //取得tab的物件
        int count = tabWidget.getChildCount();   //取得tab的分頁有幾個
        if (count > 3) {   
            for (int i = 0; i < count; i++) {   
                tabWidget.getChildTabViewAt(i)
                      .setMinimumWidth((screenWidth)/3);//設定每一個分頁最小的寬度   
            }   
        }
    }
}

圖是我從android內建的icon找的, 你可以加入你喜歡的圖示。

看起來越來越有模樣了XD

程式碼

後面還有教學
如何使用Fragment建立TabActivity之二
如何使用Fragment建立TabActivity之三