ActivityGroup在實際的開發中是十分常見的,在我使用過的Android應用中,十個應用里面有九個應用的主界面都是使用ActivityGroup的。說起ActivityGroup,在國內好像直接使用它開發的並不多,基本都是使用TabActivity,它是ActivityGroup唯一的一個子類。Android端新浪微博的主界面就是用TabActivity來實現的,還有其它的一些應用也幾乎都用TabActivity來實現。在我眼里,TabActivity是Google提供的一個非常失敗的API(至少我現在這么認為,下文我會說它失敗在哪里),但中國幾乎所有的應用都使用TabActivity,我不禁在思考這是巧合還是復制粘貼的產物。使用ActivityGroup(或者說TabActivity)開發出來的主界面效果圖如下(涉及版權問題,我這里就不粘微博的主界面了,我粘我自己的,雖然比較難看,有興趣可以去參考新浪微博,微信等Android客戶端):
可以說ActivityGroup是Google提供的一個非常優秀的API,但它需要做稍微復雜的重寫才能用起來比較方便,本文擬將實現這個稍微復雜的重寫。TabActivity作為ActivityGroup唯一的子類卻讓人大失所望。
首先來說ActivityGroup的優秀之處以及它的必要性,它為開發者提供了一種可能,這種可能不將Activity作為屏幕的頂級元素(Context)呈現,而是嵌入到ActivityGroup當中。這是一種極大的飛躍,它將場景(Context)細分化了,ActivityGroup是一個主場景,而用戶可以通過導航按鈕來切換想要的子場景。如使用微博功能,它是一個相當宏大的場景,具有看最新的廣播信息、自己發微博、修改資料等子場景,用戶可以通過按鈕來切換到想要的子場景,而這個子場景仍活動於主場景之中。讓一個主場景能擁有多個邏輯處理模塊,主場景不再負責子場景邏輯,主場景只負責切換場景的邏輯,即每一個Activity(子場景)擁有一個邏輯處理模塊,一個ActivityGroup有多個Activity,卻不干預Activity的邏輯,這無疑細分化和模塊化了邏輯代碼。ActivityGroup和它將要內嵌的Activity所要實現的功能完全可以用只一個Activity來完成,你可以試想,當你把一個ActivityGroup和它所擁有的Activity的邏輯代碼放在一個Activity中時,那這個Activity會擁有多少行代碼,為維護帶來非常的不便。
再來說說TabActivity的不足之處,首先,TabActivity自己獨有的視圖幾乎沒人使用(也就是難看的標簽頁按鈕形式),國內開發者用到的特性幾乎都是從ActivityGroup繼承下來的。還有就是TabActivity的強制依賴關系,它的布局文件必須將TabHost作根標簽,並且id必須為"@android:id/tabhost",必須有TabWidget標簽,且它的id必須是"@android:id/tabs",還有加載Activity的View容器,id必須為@android:id/tabcontent。光是強制依賴關系,我就覺得不是很舒服。不僅僅是TabActivity,在一些特殊的Activity中,如ListActivity都存在這種強制依賴關系,ListActivity必須有id為xxx(想不起來了)的ListView,我想這些弊端應該獲得Google開發者的重視。
那么我下面我就將自己實現ActivityGroup,告別強制依賴關系,並隨心所欲的建立視圖。下面這個類是一個抽象類,開發者只需對這個抽象類稍做修改,並加以實現自己的視圖就能告別TabActivity。
package com.chenjun.demo.abstracttabactivity; import android.app.Activity; import android.app.ActivityGroup; import android.app.LocalActivityManager; import android.content.Intent; import android.os.Bundle; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.RadioButton; /** * 自己實現的一個通用ActivityGroup。 * 可以通過簡單的重寫它來制作有導航按鈕和用導航按鈕控制動態加載Activity的ActivityGroup。 * 開發者需要在實現類中實現三個方法: * 1.指定動態加載Activity的容器的對象,getContainer()方法。 * 2.初始化所有的導航按鈕,initRadioBtns()方法,開發者要遍歷所有的導航按鈕並執行initRadioBtn(int id)方法。 * 3.實現導航按鈕動作監聽器的具體方法,onCheckedChanged(...)方法。這個方法將實現某個導航按鈕與要啟動對應的Activity的關聯關系,可以調用setContainerView(...)方法。 * @author zet * */ public abstract class AbstractMyActivityGroup extends ActivityGroup implements CompoundButton.OnCheckedChangeListener{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initRadioBtns(); } //加載Activity的View容器,容器應該是ViewGroup的子類 private ViewGroup container; private LocalActivityManager localActivityManager; /** * 加載Activity的View容器的id並不是固定的,將命名規則交給開發者 * 開發者可以在布局文件中自定義其id,通過重寫這個方法獲得這個View容器的對象 * @return */ abstract protected ViewGroup getContainer(); /** * 供實現類調用,根據導航按鈕id初始化按鈕 * @param id */ protected void initRadioBtn(int id){ RadioButton btn = (RadioButton) findViewById(id); btn.setOnCheckedChangeListener(this); } /** * 開發者必須重寫這個方法,來遍歷並初始化所有的導航按鈕 */ abstract protected void initRadioBtns(); /** * 為啟動Activity初始化Intent信息 * @param cls * @return */ private Intent initIntent(Class<?> cls){ return new Intent(this, cls).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); } /** * 供開發者在實現類中調用,能將Activity容器內的Activity移除,再將指定的某個Activity加入 * @param activityName 加載的Activity在localActivityManager中的名字 * @param activityClassTye 要加載Activity的類型 */ protected void setContainerView(String activityName, Class<?> activityClassTye){ if(null == localActivityManager){ localActivityManager = getLocalActivityManager(); } if(null == container){ container = getContainer(); } //移除內容部分全部的View container.removeAllViews(); Activity contentActivity = localActivityManager.getActivity(activityName); if (null == contentActivity) { localActivityManager.startActivity(activityName, initIntent(activityClassTye)); } //加載Activity container.addView( localActivityManager.getActivity(activityName) .getWindow().getDecorView(), new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); } }
需要重寫的方法以及為什么需要重寫我都已在原代碼中標明。下面我們來具體的實現這個類,來達到我們想要的預期。
package com.chenjun.demo.abstracttabactivity; import android.os.Bundle; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.RadioButton; public class TestMyActivityGroup extends AbstractMyActivityGroup{ //加載的Activity的名字,LocalActivityManager就是通過這些名字來查找對應的Activity的。 private static final String CONTENT_ACTIVITY_NAME_0 = "contentActivity0"; private static final String CONTENT_ACTIVITY_NAME_1 = "contentActivity1"; private static final String CONTENT_ACTIVITY_NAME_2 = "contentActivity2"; private static final String CONTENT_ACTIVITY_NAME_3 = "contentActivity3"; private static final String CONTENT_ACTIVITY_NAME_4 = "contentActivity4"; @Override protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.my_activity_group); super.onCreate(savedInstanceState); ((RadioButton)findViewById(R.id.radio_button0)).setChecked(true); } /** * 找到自定義id的加載Activity的View */ @Override protected ViewGroup getContainer() { return (ViewGroup) findViewById(R.id.container); } /** * 初始化按鈕 */ @Override protected void initRadioBtns() { initRadioBtn(R.id.radio_button0); initRadioBtn(R.id.radio_button1); initRadioBtn(R.id.radio_button2); initRadioBtn(R.id.radio_button3); initRadioBtn(R.id.radio_button4); } /** * 導航按鈕被點擊時,具體發生的變化 */ @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { switch (buttonView.getId()) { case R.id.radio_button0: setContainerView(CONTENT_ACTIVITY_NAME_0, ContentActivity0.class); break; case R.id.radio_button1: setContainerView(CONTENT_ACTIVITY_NAME_1, ContentActivity1.class); break; case R.id.radio_button2: setContainerView(CONTENT_ACTIVITY_NAME_2, ContentActivity2.class); break; case R.id.radio_button3: setContainerView(CONTENT_ACTIVITY_NAME_3, ContentActivity3.class); break; case R.id.radio_button4: setContainerView(CONTENT_ACTIVITY_NAME_4, ContentActivity4.class); break; default: break; } } } }
布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="0.0px" xmlns:android="http://schemas.android.com/apk/res/android"> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <LinearLayout android:id="@+id/container" android:layout_width="fill_parent" android:layout_height="0.0dip" android:layout_weight="1.0" /> <RadioGroup android:gravity="center_vertical" android:layout_gravity="bottom" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content"> <RadioButton android:id="@+id/radio_button0" android:layout_marginTop="2.0dip" android:text="按鈕1" style="@style/tab_radio" android:drawableTop="@android:drawable/ic_menu_call" /> <RadioButton android:id="@+id/radio_button1" android:layout_marginTop="2.0dip" android:text="按鈕2" style="@style/tab_radio" android:drawableTop="@android:drawable/ic_menu_camera" /> <RadioButton android:id="@+id/radio_button2" android:layout_marginTop="2.0dip" android:text="按鈕3" style="@style/tab_radio" android:drawableTop="@android:drawable/ic_menu_agenda" /> <RadioButton android:id="@+id/radio_button3" android:layout_marginTop="2.0dip" android:text="按鈕4" style="@style/tab_radio" android:drawableTop="@android:drawable/ic_menu_delete" /> <RadioButton android:id="@+id/radio_button4" android:layout_marginTop="2.0dip" android:text="按鈕5" style="@style/tab_radio" android:drawableTop="@android:drawable/ic_menu_help" /> </RadioGroup> </LinearLayout> </LinearLayout>
具體的實現效果(這里Activity基本沒有內容的,就加了一行字):
具體的代碼演示就差不多了,這里要做一些說明的:
1.開發者在自己的實現類中的onCreate方法中,必須先設置視圖,再調用super.oncreate(...)方法。具體為什么看了抽象類的源代碼我相信讀者應該會明白。
2.關於導航按鈕使用RadioButton。Android沒有特意為我們定制適合我們在這種場合下使用的按鈕,也就是上面可以設置簡筆畫,下面有文字說明。解決方案:1)使用ImageButton,將簡筆畫和文字說明P在一張圖片里面,但這樣有一個非常明顯的弊端,文字說明的文字字體是固定的,是P在圖片里的,那么不和系統的文字一樣。如果用戶使用一些比較花哨的系統文字,而導航按鈕卻是宋體,在上面的內容部分是他的系統文字,那么我很難想象他下一次是否還會打開您所開發的應用。2)自己去實現一個View,去代替RadioButton,出於學習目的這是好的。最佳的解決方案我還是認為是用RadioButton,只需對它稍做修改即可,具體可以參照新浪微博的資源文件。
缺陷反思:這些代碼都是我從重構得來的,當時開發的時候並沒有設計好開發流程(我是先有那個實現類,才有了那個抽象類的)。自己寫的ActivityGroup與TabActivity相比,優點顯而易見,缺點就是可能不穩定,但暫時沒有發現Bug,動態加載的Activity的邏輯代碼都能正確執行。
另外筆者正在求職中,有意向的聯系 answer1991.chen@gmail.com 。 非誠勿擾